Compare commits
2 Commits
develop
...
refactor/o
| Author | SHA1 | Date |
|---|---|---|
|
|
64d06c90ba | |
|
|
61e5f9bf39 |
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Package common define common error variables
|
||||||
|
package common
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrUnsupportedMeasurementField define error of unsupport measurement field
|
||||||
|
ErrUnsupportedMeasurementField = errors.New("unsupported measurement field")
|
||||||
|
// ErrInvalidMeasurementToken indicates that a token cannot represent a measurement.
|
||||||
|
ErrInvalidMeasurementToken = errors.New("invalid measurement token")
|
||||||
|
// ErrMeasurementTokenNotFound indicates that no measurement matches the token hierarchy.
|
||||||
|
ErrMeasurementTokenNotFound = errors.New("measurement token not found")
|
||||||
|
// ErrAmbiguousMeasurementToken indicates that a token matches more than one measurement.
|
||||||
|
ErrAmbiguousMeasurementToken = errors.New("ambiguous measurement token")
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Package constants define constant variable
|
||||||
|
package constants
|
||||||
|
|
||||||
|
// DataObjectType identifies the kind of object represented by a data object token.
|
||||||
|
type DataObjectType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DataObjectTypeParameter represents a component parameter.
|
||||||
|
DataObjectTypeParameter DataObjectType = "parameter"
|
||||||
|
// DataObjectTypeMeasurement represents a component measurement.
|
||||||
|
DataObjectTypeMeasurement DataObjectType = "measurement"
|
||||||
|
)
|
||||||
|
|
@ -4,8 +4,6 @@ package constants
|
||||||
const (
|
const (
|
||||||
// DefaultScore define the default score for redissearch suggestion
|
// DefaultScore define the default score for redissearch suggestion
|
||||||
DefaultScore = 1.0
|
DefaultScore = 1.0
|
||||||
// ComponentConfigKey define component config token used at token6
|
|
||||||
ComponentConfigKey = "component"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -44,9 +42,6 @@ const (
|
||||||
|
|
||||||
// RedisSpecCompTagMeasSetKey define redis set key which store all measurement tag keys under specific component tag
|
// RedisSpecCompTagMeasSetKey define redis set key which store all measurement tag keys under specific component tag
|
||||||
RedisSpecCompTagMeasSetKey = "%s_measurement_tag_keys"
|
RedisSpecCompTagMeasSetKey = "%s_measurement_tag_keys"
|
||||||
|
|
||||||
// RedisSpecCompNSPathMeasSetKey define redis set key which store all measurement tag keys under specific component nspath
|
|
||||||
RedisSpecCompNSPathMeasSetKey = "%s_nspath_measurement_tag_keys"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
// Package database define database operation functions
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"modelRT/orm"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// QueryComponentColumnNames returns all column names from the component table.
|
|
||||||
func QueryComponentColumnNames(ctx context.Context, db *gorm.DB) ([]string, error) {
|
|
||||||
columnTypes, err := db.WithContext(ctx).Migrator().ColumnTypes((&orm.Component{}).TableName())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
columnNames := make([]string, 0, len(columnTypes))
|
|
||||||
for _, columnType := range columnTypes {
|
|
||||||
columnName := columnType.Name()
|
|
||||||
if columnName == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
columnNames = append(columnNames, columnName)
|
|
||||||
}
|
|
||||||
return columnNames, nil
|
|
||||||
}
|
|
||||||
|
|
@ -4,23 +4,174 @@ package database
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"modelRT/common"
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
|
"modelRT/sql"
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ZoneWithParent struct {
|
// QueryMeasurementByID return the result of query circuit diagram component measurement info by id from postgresDB
|
||||||
orm.Zone
|
func QueryMeasurementByID(ctx context.Context, tx *gorm.DB, id int64) (orm.Measurement, error) {
|
||||||
GridTag string `gorm:"column:grid_tag"`
|
var measurement orm.Measurement
|
||||||
|
// ctx超时判断
|
||||||
|
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
result := tx.WithContext(cancelCtx).
|
||||||
|
Where(sql.MeasurementIDWhere, id).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
First(&measurement)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return orm.Measurement{}, result.Error
|
||||||
|
}
|
||||||
|
return measurement, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type StationWithParent struct {
|
// QueryMeasurementByToken define function query circuit diagram component measurement info by token from postgresDB
|
||||||
orm.Zone
|
func QueryMeasurementByToken(ctx context.Context, tx *gorm.DB, token string) (orm.Measurement, error) {
|
||||||
ZoneTag string `gorm:"column:zone_tag"`
|
measurement, _, err := QueryMeasurementByDataObjectToken(ctx, tx, token)
|
||||||
|
if err != nil {
|
||||||
|
return orm.Measurement{}, err
|
||||||
|
}
|
||||||
|
return *measurement, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateMeasurementToken checks whether token uniquely identifies an existing
|
||||||
|
// measurement through the measurement, component, bay, station, zone, and grid
|
||||||
|
// relationships. Supported formats are token1-token7, token4-token7, and
|
||||||
|
// token4.token7.
|
||||||
|
func ValidateMeasurementToken(ctx context.Context, db *gorm.DB, token string) error {
|
||||||
|
query, args, err := buildMeasurementTokenValidationQuery(token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := db.WithContext(ctx).Raw(query, args...).Scan(&count).Error; err != nil {
|
||||||
|
return fmt.Errorf("query measurement token %q: %w", token, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case count == 0:
|
||||||
|
return fmt.Errorf("%w: %q", common.ErrMeasurementTokenNotFound, token)
|
||||||
|
case count > 1:
|
||||||
|
return fmt.Errorf("%w: %q matched %d records", common.ErrAmbiguousMeasurementToken, token, count)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryMeasurementByDataObjectToken validates token and returns the existing
|
||||||
|
// measurement and its owning component for attribute response construction.
|
||||||
|
func QueryMeasurementByDataObjectToken(ctx context.Context, db *gorm.DB, token string) (*orm.Measurement, *orm.Component, error) {
|
||||||
|
validationQuery, args, err := buildMeasurementTokenValidationQuery(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := buildMeasurementRowsQuery(validationQuery)
|
||||||
|
var rows []orm.Measurement
|
||||||
|
if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("query measurement token %q: %w", token, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(rows) {
|
||||||
|
case 0:
|
||||||
|
return nil, nil, fmt.Errorf("%w: %q", common.ErrMeasurementTokenNotFound, token)
|
||||||
|
case 1:
|
||||||
|
// Continue by loading the owning component.
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("%w: %q matched more than one record", common.ErrAmbiguousMeasurementToken, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
var component orm.Component
|
||||||
|
result := db.WithContext(ctx).
|
||||||
|
Raw(compactMeasurementSQL(sql.MeasurementComponentByUUID), rows[0].ComponentUUID).
|
||||||
|
Scan(&component)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, nil, fmt.Errorf("query component for measurement token %q: %w", token, result.Error)
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return nil, nil, fmt.Errorf("%w: component for %q", common.ErrMeasurementTokenNotFound, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &rows[0], &component, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMeasurementRowsQuery(validationQuery string) string {
|
||||||
|
measurementQuery := strings.Replace(
|
||||||
|
validationQuery,
|
||||||
|
sql.MeasurementCountSelect,
|
||||||
|
sql.MeasurementRowsSelect,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
return compactMeasurementSQL(strings.Join([]string{measurementQuery, sql.MeasurementLimitTwo}, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func compactMeasurementSQL(statement string) string {
|
||||||
|
return strings.Join(strings.Fields(statement), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMeasurementTokenValidationQuery(token string) (string, []any, error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
for _, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
return "", nil, fmt.Errorf("%w %q: token segment cannot be empty", common.ErrInvalidMeasurementToken, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(parts) {
|
||||||
|
case 7:
|
||||||
|
if parts[5] != "bay" {
|
||||||
|
return "", nil, fmt.Errorf("%w %q: token6 must be bay", common.ErrInvalidMeasurementToken, token)
|
||||||
|
}
|
||||||
|
query := compactMeasurementSQL(strings.Join([]string{
|
||||||
|
sql.MeasurementTokenValidationQueryBase,
|
||||||
|
sql.MeasurementSevenPartTokenWhere,
|
||||||
|
}, "\n"))
|
||||||
|
return query, []any{parts[0], parts[1], parts[2], parts[3], parts[4], parts[6]}, nil
|
||||||
|
case 4:
|
||||||
|
if parts[2] != "bay" {
|
||||||
|
return "", nil, fmt.Errorf("%w %q: token6 must be bay", common.ErrInvalidMeasurementToken, token)
|
||||||
|
}
|
||||||
|
query := compactMeasurementSQL(strings.Join([]string{
|
||||||
|
sql.MeasurementTokenValidationQueryBase,
|
||||||
|
sql.MeasurementFourPartTokenWhere,
|
||||||
|
}, "\n"))
|
||||||
|
return query, []any{parts[0], parts[1], parts[3]}, nil
|
||||||
|
case 2:
|
||||||
|
query := compactMeasurementSQL(strings.Join([]string{
|
||||||
|
sql.MeasurementTokenValidationQueryBase,
|
||||||
|
sql.MeasurementTwoPartTokenWhere,
|
||||||
|
}, "\n"))
|
||||||
|
return query, []any{parts[0], parts[1]}, nil
|
||||||
|
default:
|
||||||
|
return "", nil, fmt.Errorf("%w %q: expected 2, 4, or 7 segments, got %d", common.ErrInvalidMeasurementToken, token, len(parts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllMeasurements define func to query all measurement info from postgresDB
|
||||||
|
func GetAllMeasurements(ctx context.Context, tx *gorm.DB) ([]orm.Measurement, error) {
|
||||||
|
var measurements []orm.Measurement
|
||||||
|
|
||||||
|
// ctx超时判断
|
||||||
|
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&measurements)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return measurements, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFullMeasurementSet queries all hierarchy tags required to build
|
||||||
|
// measurement recommendations.
|
||||||
func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSet, error) {
|
func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSet, error) {
|
||||||
mSet := &orm.MeasurementSet{
|
mSet := &orm.MeasurementSet{
|
||||||
GridToZoneTags: make(map[string][]string),
|
GridToZoneTags: make(map[string][]string),
|
||||||
|
|
@ -28,7 +179,6 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
StationToCompNSPaths: make(map[string][]string),
|
StationToCompNSPaths: make(map[string][]string),
|
||||||
CompNSPathToCompTags: make(map[string][]string),
|
CompNSPathToCompTags: make(map[string][]string),
|
||||||
CompTagToMeasTags: make(map[string][]string),
|
CompTagToMeasTags: make(map[string][]string),
|
||||||
CompNSPathToMeasTags: make(map[string][]string),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g, gctx := errgroup.WithContext(ctx)
|
g, gctx := errgroup.WithContext(ctx)
|
||||||
|
|
@ -36,7 +186,7 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
var grids []orm.Grid
|
var grids []orm.Grid
|
||||||
if err := db.Table("grid").Select("tagname").Scan(&grids).Error; err != nil {
|
if err := db.Raw(compactMeasurementSQL(sql.MeasurementGridTags)).Scan(&grids).Error; err != nil {
|
||||||
return fmt.Errorf("query grids: %w", err)
|
return fmt.Errorf("query grids: %w", err)
|
||||||
}
|
}
|
||||||
for _, grid := range grids {
|
for _, grid := range grids {
|
||||||
|
|
@ -52,16 +202,13 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
orm.Zone
|
orm.Zone
|
||||||
GridTag string `gorm:"column:grid_tag"`
|
GridTag string `gorm:"column:grid_tag"`
|
||||||
}
|
}
|
||||||
if err := db.Table("zone").
|
if err := db.Raw(compactMeasurementSQL(sql.MeasurementZoneHierarchy)).Scan(&zones).Error; err != nil {
|
||||||
Select("zone.*, grid.tagname as grid_tag").
|
|
||||||
Joins("left join grid on zone.grid_id = grid.id").
|
|
||||||
Scan(&zones).Error; err != nil {
|
|
||||||
return fmt.Errorf("query zones: %w", err)
|
return fmt.Errorf("query zones: %w", err)
|
||||||
}
|
}
|
||||||
for _, z := range zones {
|
for _, zone := range zones {
|
||||||
mSet.AllZoneTags = append(mSet.AllZoneTags, z.TAGNAME)
|
mSet.AllZoneTags = append(mSet.AllZoneTags, zone.TAGNAME)
|
||||||
if z.GridTag != "" {
|
if zone.GridTag != "" {
|
||||||
mSet.GridToZoneTags[z.GridTag] = append(mSet.GridToZoneTags[z.GridTag], z.TAGNAME)
|
mSet.GridToZoneTags[zone.GridTag] = append(mSet.GridToZoneTags[zone.GridTag], zone.TAGNAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -72,40 +219,40 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
orm.Station
|
orm.Station
|
||||||
ZoneTag string `gorm:"column:zone_tag"`
|
ZoneTag string `gorm:"column:zone_tag"`
|
||||||
}
|
}
|
||||||
if err := db.Table("station").
|
if err := db.Raw(compactMeasurementSQL(sql.MeasurementStationHierarchy)).Scan(&stations).Error; err != nil {
|
||||||
Select("station.*, zone.tagname as zone_tag").
|
|
||||||
Joins("left join zone on station.zone_id = zone.id").
|
|
||||||
Scan(&stations).Error; err != nil {
|
|
||||||
return fmt.Errorf("query stations: %w", err)
|
return fmt.Errorf("query stations: %w", err)
|
||||||
}
|
}
|
||||||
for _, s := range stations {
|
for _, station := range stations {
|
||||||
mSet.AllStationTags = append(mSet.AllStationTags, s.TAGNAME)
|
mSet.AllStationTags = append(mSet.AllStationTags, station.TAGNAME)
|
||||||
if s.ZoneTag != "" {
|
if station.ZoneTag != "" {
|
||||||
mSet.ZoneToStationTags[s.ZoneTag] = append(mSet.ZoneToStationTags[s.ZoneTag], s.TAGNAME)
|
mSet.ZoneToStationTags[station.ZoneTag] = append(mSet.ZoneToStationTags[station.ZoneTag], station.TAGNAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
var comps []struct {
|
var components []struct {
|
||||||
orm.Component
|
orm.Component
|
||||||
StationTag string `gorm:"column:station_tag"`
|
StationTag string `gorm:"column:station_tag"`
|
||||||
}
|
}
|
||||||
if err := db.Table("component").
|
if err := db.Raw(compactMeasurementSQL(sql.MeasurementComponentHierarchy)).Scan(&components).Error; err != nil {
|
||||||
Select("component.*, station.tagname as station_tag").
|
|
||||||
Joins("left join station on component.station_id = station.id").
|
|
||||||
Scan(&comps).Error; err != nil {
|
|
||||||
return fmt.Errorf("query components: %w", err)
|
return fmt.Errorf("query components: %w", err)
|
||||||
}
|
}
|
||||||
for _, c := range comps {
|
for _, component := range components {
|
||||||
mSet.AllCompNSPaths = append(mSet.AllCompNSPaths, c.NSPath)
|
mSet.AllCompNSPaths = append(mSet.AllCompNSPaths, component.NSPath)
|
||||||
mSet.AllCompTags = append(mSet.AllCompTags, c.Tag)
|
mSet.AllCompTags = append(mSet.AllCompTags, component.Tag)
|
||||||
if c.StationTag != "" {
|
if component.StationTag != "" {
|
||||||
mSet.StationToCompNSPaths[c.StationTag] = append(mSet.StationToCompNSPaths[c.StationTag], c.NSPath)
|
mSet.StationToCompNSPaths[component.StationTag] = append(
|
||||||
|
mSet.StationToCompNSPaths[component.StationTag],
|
||||||
|
component.NSPath,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if c.NSPath != "" {
|
if component.NSPath != "" {
|
||||||
mSet.CompNSPathToCompTags[c.NSPath] = append(mSet.CompNSPathToCompTags[c.NSPath], c.Tag)
|
mSet.CompNSPathToCompTags[component.NSPath] = append(
|
||||||
|
mSet.CompNSPathToCompTags[component.NSPath],
|
||||||
|
component.Tag,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -114,24 +261,18 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
var measurements []struct {
|
var measurements []struct {
|
||||||
orm.Measurement
|
orm.Measurement
|
||||||
CompTag string `gorm:"column:comp_tag"`
|
CompTag string `gorm:"column:comp_tag"`
|
||||||
CompNSPath string `gorm:"column:comp_nspath"`
|
|
||||||
BayTag string `gorm:"column:bay_tag"`
|
|
||||||
}
|
}
|
||||||
if err := db.Table("measurement").
|
if err := db.Raw(compactMeasurementSQL(sql.MeasurementTagHierarchy)).Scan(&measurements).Error; err != nil {
|
||||||
Select("measurement.*, component.tag as comp_tag, component.nspath as comp_nspath, bay.tag as bay_tag").
|
|
||||||
Joins("left join component on measurement.component_uuid = component.global_uuid").
|
|
||||||
Joins("left join bay on measurement.bay_uuid = bay.bay_uuid").
|
|
||||||
Scan(&measurements).Error; err != nil {
|
|
||||||
return fmt.Errorf("query measurements: %w", err)
|
return fmt.Errorf("query measurements: %w", err)
|
||||||
}
|
}
|
||||||
for _, m := range measurements {
|
for _, measurement := range measurements {
|
||||||
mSet.AllMeasTags = append(mSet.AllMeasTags, m.Tag)
|
mSet.AllMeasTags = append(mSet.AllMeasTags, measurement.Tag)
|
||||||
if m.CompTag != "" {
|
if measurement.CompTag != "" {
|
||||||
mSet.CompTagToMeasTags[m.CompTag] = append(mSet.CompTagToMeasTags[m.CompTag], m.Tag)
|
mSet.CompTagToMeasTags[measurement.CompTag] = append(
|
||||||
}
|
mSet.CompTagToMeasTags[measurement.CompTag],
|
||||||
if m.CompNSPath != "" && m.CompNSPath == m.BayTag {
|
measurement.Tag,
|
||||||
mSet.CompNSPathToMeasTags[m.CompNSPath] = append(mSet.CompNSPathToMeasTags[m.CompNSPath], m.Tag)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"modelRT/common"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildMeasurementTokenValidationQuery(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
token string
|
||||||
|
wantArgs []any
|
||||||
|
wantWhere string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "seven-part token",
|
||||||
|
token: "grid.zone.station.nspath.component.bay.measurement",
|
||||||
|
wantArgs: []any{"grid", "zone", "station", "nspath", "component", "measurement"},
|
||||||
|
wantWhere: "WHERE g.tagname = ?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "four-part token",
|
||||||
|
token: "nspath.component.bay.measurement",
|
||||||
|
wantArgs: []any{"nspath", "component", "measurement"},
|
||||||
|
wantWhere: "WHERE c.nspath = ?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two-part token",
|
||||||
|
token: "nspath.measurement",
|
||||||
|
wantArgs: []any{"nspath", "measurement"},
|
||||||
|
wantWhere: "WHERE c.nspath = ?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-bay group",
|
||||||
|
token: "nspath.component.rated.attribute",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty segment",
|
||||||
|
token: "nspath..bay.measurement",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid segment count",
|
||||||
|
token: "grid.zone.station",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
query, args, err := buildMeasurementTokenValidationQuery(tt.token)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, common.ErrInvalidMeasurementToken)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, query, "INNER JOIN component AS c ON c.global_uuid = m.component_uuid")
|
||||||
|
assert.Contains(t, query, "INNER JOIN bay AS b ON b.bay_uuid = m.bay_uuid")
|
||||||
|
assert.Contains(t, query, tt.wantWhere)
|
||||||
|
assert.NotContains(t, query, "grid_idWHERE")
|
||||||
|
assert.Regexp(t, `grid_id\s+WHERE`, query)
|
||||||
|
assert.NotContains(t, query, "\n")
|
||||||
|
assert.NotContains(t, query, "\t")
|
||||||
|
assert.Equal(t, tt.wantArgs, args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateMeasurementToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
count int64
|
||||||
|
queryErr error
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{name: "exists", count: 1},
|
||||||
|
{name: "not found", count: 0, wantErr: common.ErrMeasurementTokenNotFound},
|
||||||
|
{name: "ambiguous", count: 2, wantErr: common.ErrAmbiguousMeasurementToken},
|
||||||
|
{name: "query failure", queryErr: errors.New("database unavailable")},
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = "nspath.measurement"
|
||||||
|
query, _, err := buildMeasurementTokenValidationQuery(token)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedQuery := query
|
||||||
|
for i := 1; strings.Contains(expectedQuery, "?"); i++ {
|
||||||
|
expectedQuery = strings.Replace(expectedQuery, "?", fmt.Sprintf("$%d", i), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
sqlDB, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||||
|
|
||||||
|
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectation := mock.ExpectQuery(regexp.QuoteMeta(expectedQuery)).
|
||||||
|
WithArgs("nspath", "measurement")
|
||||||
|
if tt.queryErr != nil {
|
||||||
|
expectation.WillReturnError(tt.queryErr)
|
||||||
|
} else {
|
||||||
|
expectation.WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.count))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ValidateMeasurementToken(context.Background(), db, token)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
assert.ErrorIs(t, err, tt.wantErr)
|
||||||
|
} else if tt.queryErr != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.queryErr.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMeasurementRowsQuerySeparatesLimitClause(t *testing.T) {
|
||||||
|
validationQuery, _, err := buildMeasurementTokenValidationQuery("nspath.measurement")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
query := buildMeasurementRowsQuery(validationQuery)
|
||||||
|
assert.NotContains(t, query, "?LIMIT")
|
||||||
|
assert.Regexp(t, `m\.tag = \?\s+LIMIT 2$`, query)
|
||||||
|
assert.NotContains(t, query, "\n")
|
||||||
|
assert.NotContains(t, query, "\t")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryMeasurementByDataObjectToken(t *testing.T) {
|
||||||
|
sqlDB, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||||
|
|
||||||
|
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
const componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
||||||
|
mock.ExpectQuery(`(?s)SELECT m\.\*.*WHERE c\.nspath = \$1.*AND m\.tag = \$2.*LIMIT 2`).
|
||||||
|
WithArgs("nspath", "measurement").
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
|
"id",
|
||||||
|
"tag",
|
||||||
|
"name",
|
||||||
|
"mode",
|
||||||
|
"size",
|
||||||
|
"data_source",
|
||||||
|
"event_plan",
|
||||||
|
"binding",
|
||||||
|
"component_uuid",
|
||||||
|
}).AddRow(
|
||||||
|
int64(10),
|
||||||
|
"measurement",
|
||||||
|
"A phase current",
|
||||||
|
int16(1),
|
||||||
|
10,
|
||||||
|
`{"type":1,"io_address":{"channel":"tm1"}}`,
|
||||||
|
`{"enabled":true}`,
|
||||||
|
`{"ct":{"ratio":1}}`,
|
||||||
|
componentUUID,
|
||||||
|
))
|
||||||
|
mock.ExpectQuery(`(?s)SELECT global_uuid, nspath, tag, grid, zone, station.*WHERE global_uuid = \$1.*LIMIT 1`).
|
||||||
|
WithArgs(componentUUID).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
|
"global_uuid",
|
||||||
|
"nspath",
|
||||||
|
"tag",
|
||||||
|
"grid",
|
||||||
|
"zone",
|
||||||
|
"station",
|
||||||
|
}).AddRow(componentUUID, "nspath", "component", "grid", "zone", "station"))
|
||||||
|
|
||||||
|
measurement, component, err := QueryMeasurementByDataObjectToken(context.Background(), db, "nspath.measurement")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(10), measurement.ID)
|
||||||
|
assert.Equal(t, int16(1), measurement.Mode)
|
||||||
|
assert.Equal(t, float64(1), measurement.DataSource["type"])
|
||||||
|
assert.Equal(t, "grid", component.GridName)
|
||||||
|
assert.Equal(t, "component", component.Tag)
|
||||||
|
require.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
// 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 measurement 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(&measurement)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
return orm.Measurement{}, result.Error
|
|
||||||
}
|
|
||||||
return measurement, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryMeasurementByToken define function query circuit diagram component measurement info by token from postgresDB
|
|
||||||
func QueryMeasurementByToken(ctx context.Context, tx *gorm.DB, token string) (orm.Measurement, error) {
|
|
||||||
// TODO parse token to avoid SQL injection
|
|
||||||
|
|
||||||
var component orm.Measurement
|
|
||||||
// ctx超时判断
|
|
||||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
result := tx.WithContext(cancelCtx).
|
|
||||||
Where(" = ?", token).
|
|
||||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
First(&component)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
return orm.Measurement{}, result.Error
|
|
||||||
}
|
|
||||||
return component, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllMeasurements define func to query all measurement info from postgresDB
|
|
||||||
func GetAllMeasurements(ctx context.Context, tx *gorm.DB) ([]orm.Measurement, error) {
|
|
||||||
var measurements []orm.Measurement
|
|
||||||
|
|
||||||
// ctx超时判断
|
|
||||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&measurements)
|
|
||||||
if result.Error != nil {
|
|
||||||
return nil, result.Error
|
|
||||||
}
|
|
||||||
return measurements, nil
|
|
||||||
}
|
|
||||||
|
|
@ -88,7 +88,7 @@ func generateNormalData(baseValue, normalBase float64) []float64 {
|
||||||
func main() {
|
func main() {
|
||||||
rootCtx := context.Background()
|
rootCtx := context.Background()
|
||||||
|
|
||||||
pgURI := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", "192.168.1.101", 5432, "postgres", "coslight", "demo")
|
pgURI := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", "localhost", 5432, "postgres", "coslight", "develop_env")
|
||||||
|
|
||||||
postgresDBClient, err := gorm.Open(postgres.Open(pgURI))
|
postgresDBClient, err := gorm.Open(postgres.Open(pgURI))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -164,7 +164,6 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
datas = generateMixedData(highMin, lowMin, highBase, lowBase, baseValue, normalBase)
|
datas = generateMixedData(highMin, lowMin, highBase, lowBase, baseValue, normalBase)
|
||||||
// log.Printf("key:%s\n datas:%v\n", key, datas)
|
|
||||||
|
|
||||||
allHigh := true
|
allHigh := true
|
||||||
for i := highStart; i < highEnd; i++ {
|
for i := highStart; i < highEnd; i++ {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
)
|
)
|
||||||
|
|
@ -61,7 +62,7 @@ func ProcessMeasurements(measurements []orm.Measurement) map[string]CalculationR
|
||||||
device, _ := ioAddress["device"].(string)
|
device, _ := ioAddress["device"].(string)
|
||||||
channel, _ := ioAddress["channel"].(string)
|
channel, _ := ioAddress["channel"].(string)
|
||||||
|
|
||||||
result := fmt.Sprintf("%s:%s:phasor:%s", station, device, channel)
|
result := strings.ToLower(fmt.Sprintf("%s:%s:phasor:%s", station, device, channel))
|
||||||
if measurement.EventPlan == nil {
|
if measurement.EventPlan == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package diagram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
@ -12,6 +14,46 @@ type RedisClient struct {
|
||||||
Client *redis.Client
|
Client *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueryLatestMeasurementValue returns the score whose member contains the
|
||||||
|
// greatest numeric timestamp. Measurement ZSets currently store timestamp in
|
||||||
|
// member and measurement value in score.
|
||||||
|
func (rc *RedisClient) QueryLatestMeasurementValue(ctx context.Context, key string) (float64, error) {
|
||||||
|
if rc.Client == nil {
|
||||||
|
return 0, fmt.Errorf("redis client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
members, err := rc.Client.ZRangeWithScores(ctx, key, 0, -1).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return latestMeasurementValue(members, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func latestMeasurementValue(members []redis.Z, key string) (float64, error) {
|
||||||
|
if len(members) == 0 {
|
||||||
|
return 0, fmt.Errorf("real-time measurement value not found for key %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestTimestamp int64
|
||||||
|
var latestValue float64
|
||||||
|
found := false
|
||||||
|
for _, member := range members {
|
||||||
|
timestamp, err := strconv.ParseInt(fmt.Sprint(member.Member), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !found || timestamp > latestTimestamp {
|
||||||
|
latestTimestamp = timestamp
|
||||||
|
latestValue = member.Score
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return 0, fmt.Errorf("real-time measurement timestamps are invalid for key %q", key)
|
||||||
|
}
|
||||||
|
return latestValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewRedisClient define func of new redis client instance
|
// NewRedisClient define func of new redis client instance
|
||||||
func NewRedisClient() *RedisClient {
|
func NewRedisClient() *RedisClient {
|
||||||
return &RedisClient{
|
return &RedisClient{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package diagram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLatestMeasurementValueUsesMemberTimestamp(t *testing.T) {
|
||||||
|
value, err := latestMeasurementValue([]redis.Z{
|
||||||
|
{Member: "100", Score: 999},
|
||||||
|
{Member: "300", Score: 12},
|
||||||
|
{Member: "200", Score: 500},
|
||||||
|
}, "measurement-key")
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, float64(12), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLatestMeasurementValueRejectsMissingOrInvalidTimestamps(t *testing.T) {
|
||||||
|
_, err := latestMeasurementValue(nil, "measurement-key")
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
_, err = latestMeasurementValue([]redis.Z{{Member: "invalid", Score: 1}}, "measurement-key")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
@ -524,6 +524,10 @@ const docTemplate = `{
|
||||||
" \"I_B_rms\"",
|
" \"I_B_rms\"",
|
||||||
"\"I_C_rms\"]"
|
"\"I_C_rms\"]"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"recommended_type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "grid_tag"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -518,6 +518,10 @@
|
||||||
" \"I_B_rms\"",
|
" \"I_B_rms\"",
|
||||||
"\"I_C_rms\"]"
|
"\"I_C_rms\"]"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"recommended_type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "grid_tag"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,9 @@ definitions:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
recommended_type:
|
||||||
|
example: grid_tag
|
||||||
|
type: string
|
||||||
type: object
|
type: object
|
||||||
network.RealTimeDataPayload:
|
network.RealTimeDataPayload:
|
||||||
properties:
|
properties:
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,13 @@ import (
|
||||||
|
|
||||||
// QueryAlertEventHandler define query alert event process API
|
// QueryAlertEventHandler define query alert event process API
|
||||||
func QueryAlertEventHandler(c *gin.Context) {
|
func QueryAlertEventHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var targetLevel constants.AlertLevel
|
var targetLevel constants.AlertLevel
|
||||||
|
|
||||||
alertManger := alert.GetAlertMangerInstance()
|
alertManger := alert.GetAlertMangerInstance()
|
||||||
levelStr := c.Query("level")
|
levelStr := c.Query("level")
|
||||||
level, err := strconv.Atoi(levelStr)
|
level, err := strconv.Atoi(levelStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "convert alert level string to int failed", "error", err)
|
logger.Error(c, "convert alert level string to int failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: -1,
|
Code: -1,
|
||||||
|
|
|
||||||
|
|
@ -19,16 +19,15 @@ import (
|
||||||
|
|
||||||
// ComponentAnchorReplaceHandler define component anchor point replace process API
|
// ComponentAnchorReplaceHandler define component anchor point replace process API
|
||||||
func ComponentAnchorReplaceHandler(c *gin.Context) {
|
func ComponentAnchorReplaceHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var uuid, anchorName string
|
var uuid, anchorName string
|
||||||
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
cancelCtx, cancel := context.WithTimeout(c, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var request network.ComponetAnchorReplaceRequest
|
var request network.ComponetAnchorReplaceRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "unmarshal component anchor point replace info failed", "error", err)
|
logger.Error(c, "unmarshal component anchor point replace info failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -43,7 +42,7 @@ func ComponentAnchorReplaceHandler(c *gin.Context) {
|
||||||
var componentInfo orm.Component
|
var componentInfo orm.Component
|
||||||
result := pgClient.WithContext(cancelCtx).Model(&orm.Component{}).Where("global_uuid = ?", uuid).Find(&componentInfo)
|
result := pgClient.WithContext(cancelCtx).Model(&orm.Component{}).Where("global_uuid = ?", uuid).Find(&componentInfo)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
logger.Error(ctx, "query component detail info failed", "error", result.Error)
|
logger.Error(c, "query component detail info failed", "error", result.Error)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -55,7 +54,7 @@ func ComponentAnchorReplaceHandler(c *gin.Context) {
|
||||||
|
|
||||||
if result.RowsAffected == 0 {
|
if result.RowsAffected == 0 {
|
||||||
err := fmt.Errorf("query component detail info by uuid failed:%w", errcode.ErrQueryRowZero)
|
err := fmt.Errorf("query component detail info by uuid failed:%w", errcode.ErrQueryRowZero)
|
||||||
logger.Error(ctx, "query component detail info from table is empty", "table_name", "component")
|
logger.Error(c, "query component detail info from table is empty", "table_name", "component")
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,7 @@ func validateBatchImportParams(params map[string]any) bool {
|
||||||
func validateTestTaskParams(params map[string]any) bool {
|
func validateTestTaskParams(params map[string]any) bool {
|
||||||
// Test task has optional parameters, all are valid
|
// Test task has optional parameters, all are valid
|
||||||
// sleep_duration defaults to 60 seconds if not provided
|
// sleep_duration defaults to 60 seconds if not provided
|
||||||
// TODO Add more validation logic for test task parameters if needed
|
fmt.Println("Test task parameters:", params)
|
||||||
fmt.Println(params)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,12 @@ import (
|
||||||
|
|
||||||
// AttrDeleteHandler deletes a data attribute
|
// AttrDeleteHandler deletes a data attribute
|
||||||
func AttrDeleteHandler(c *gin.Context) {
|
func AttrDeleteHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.AttrDeleteRequest
|
var request network.AttrDeleteRequest
|
||||||
clientToken := c.GetString("client_token")
|
clientToken := c.GetString("client_token")
|
||||||
if clientToken == "" {
|
if clientToken == "" {
|
||||||
err := common.ErrGetClientToken
|
err := common.ErrGetClientToken
|
||||||
|
|
||||||
logger.Error(ctx, "failed to get client token from context", "error", err)
|
logger.Error(c, "failed to get client token from context", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -28,7 +27,7 @@ func AttrDeleteHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to unmarshal attribute delete request", "error", err)
|
logger.Error(c, "failed to unmarshal attribute delete request", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -36,9 +35,9 @@ func AttrDeleteHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rs := diagram.NewRedisString(ctx, request.AttrToken, clientToken, 10, true)
|
rs := diagram.NewRedisString(c, request.AttrToken, clientToken, 10, true)
|
||||||
if err := rs.GETDEL(request.AttrToken); err != nil {
|
if err := rs.GETDEL(request.AttrToken); err != nil {
|
||||||
logger.Error(ctx, "failed to delete attribute from Redis", "attr_token", request.AttrToken, "error", err)
|
logger.Error(c, "failed to delete attribute from Redis", "attr_token", request.AttrToken, "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,13 @@ import (
|
||||||
|
|
||||||
// AttrGetHandler retrieves the value of a data attribute
|
// AttrGetHandler retrieves the value of a data attribute
|
||||||
func AttrGetHandler(c *gin.Context) {
|
func AttrGetHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.AttrGetRequest
|
var request network.AttrGetRequest
|
||||||
|
|
||||||
clientToken := c.GetString("client_token")
|
clientToken := c.GetString("client_token")
|
||||||
if clientToken == "" {
|
if clientToken == "" {
|
||||||
err := common.ErrGetClientToken
|
err := common.ErrGetClientToken
|
||||||
|
|
||||||
logger.Error(ctx, "failed to get client token from context", "error", err)
|
logger.Error(c, "failed to get client token from context", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -29,7 +28,7 @@ func AttrGetHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to unmarshal attribute get request", "error", err)
|
logger.Error(c, "failed to unmarshal attribute get request", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -38,12 +37,12 @@ func AttrGetHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
tx := pgClient.WithContext(ctx).Begin()
|
tx := pgClient.Begin()
|
||||||
|
|
||||||
attrModel, err := database.ParseAttrToken(ctx, tx, request.AttrToken, clientToken)
|
attrModel, err := database.ParseAttrToken(c, tx, request.AttrToken, clientToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
logger.Error(ctx, "failed to parse attribute token", "attr_token", request.AttrToken, "error", err)
|
logger.Error(c, "failed to parse attribute token", "attr_token", request.AttrToken, "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,13 @@ import (
|
||||||
|
|
||||||
// AttrSetHandler sets the value of a data attribute
|
// AttrSetHandler sets the value of a data attribute
|
||||||
func AttrSetHandler(c *gin.Context) {
|
func AttrSetHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.AttrSetRequest
|
var request network.AttrSetRequest
|
||||||
|
|
||||||
clientToken := c.GetString("client_token")
|
clientToken := c.GetString("client_token")
|
||||||
if clientToken == "" {
|
if clientToken == "" {
|
||||||
err := common.ErrGetClientToken
|
err := common.ErrGetClientToken
|
||||||
|
|
||||||
logger.Error(ctx, "failed to get client token from context", "error", err)
|
logger.Error(c, "failed to get client token from context", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -29,7 +28,7 @@ func AttrSetHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to unmarshal attribute set request", "error", err)
|
logger.Error(c, "failed to unmarshal attribute set request", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -38,9 +37,9 @@ func AttrSetHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The logic for handling Redis operations directly from the handler
|
// The logic for handling Redis operations directly from the handler
|
||||||
rs := diagram.NewRedisString(ctx, request.AttrToken, clientToken, 10, true)
|
rs := diagram.NewRedisString(c, request.AttrToken, clientToken, 10, true)
|
||||||
if err := rs.Set(request.AttrToken, request.AttrValue); err != nil {
|
if err := rs.Set(request.AttrToken, request.AttrValue); err != nil {
|
||||||
logger.Error(ctx, "failed to set attribute value in Redis", "attr_token", request.AttrToken, "error", err)
|
logger.Error(c, "failed to set attribute value in Redis", "attr_token", request.AttrToken, "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,11 @@ import (
|
||||||
|
|
||||||
// CircuitDiagramCreateHandler define circuit diagram create process API
|
// CircuitDiagramCreateHandler define circuit diagram create process API
|
||||||
func CircuitDiagramCreateHandler(c *gin.Context) {
|
func CircuitDiagramCreateHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
|
|
||||||
var request network.CircuitDiagramCreateRequest
|
var request network.CircuitDiagramCreateRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "unmarshal circuit diagram create info failed", "error", err)
|
logger.Error(c, "unmarshal circuit diagram create info failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -33,7 +32,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
|
||||||
|
|
||||||
graph, err := diagram.GetGraphMap(request.PageID)
|
graph, err := diagram.GetGraphMap(request.PageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
|
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -61,7 +60,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
|
||||||
err = fmt.Errorf("convert uuid from string failed:%w:%w", err1, err2)
|
err = fmt.Errorf("convert uuid from string failed:%w:%w", err1, err2)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Error(ctx, "format uuid from string failed", "error", err)
|
logger.Error(c, "format uuid from string failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -79,13 +78,13 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// open transaction
|
// open transaction
|
||||||
tx := pgClient.WithContext(ctx).Begin()
|
tx := pgClient.Begin()
|
||||||
|
|
||||||
err = database.CreateTopologicIntoDB(ctx, tx, request.PageID, topologicCreateInfos)
|
err = database.CreateTopologicIntoDB(c, tx, request.PageID, topologicCreateInfos)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|
||||||
logger.Error(ctx, "create topologic info into DB failed", "topologic_info", topologicCreateInfos, "error", err)
|
logger.Error(c, "create topologic info into DB failed", "topologic_info", topologicCreateInfos, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -103,11 +102,11 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for index, info := range request.ComponentInfos {
|
for index, info := range request.ComponentInfos {
|
||||||
componentUUID, err := database.CreateComponentIntoDB(ctx, tx, info)
|
componentUUID, err := database.CreateComponentIntoDB(c, tx, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|
||||||
logger.Error(ctx, "insert component info into DB failed", "error", err)
|
logger.Error(c, "insert component info into DB failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -126,7 +125,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
|
||||||
// TODO 修复赋值问题
|
// TODO 修复赋值问题
|
||||||
component, err := network.ConvertComponentCreateInfosToComponents(info)
|
component, err := network.ConvertComponentCreateInfosToComponents(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "convert component params info failed", "component_info", info, "error", err)
|
logger.Error(c, "convert component params info failed", "component_info", info, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,11 @@ import (
|
||||||
|
|
||||||
// CircuitDiagramDeleteHandler define circuit diagram delete process API
|
// CircuitDiagramDeleteHandler define circuit diagram delete process API
|
||||||
func CircuitDiagramDeleteHandler(c *gin.Context) {
|
func CircuitDiagramDeleteHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
|
|
||||||
var request network.CircuitDiagramDeleteRequest
|
var request network.CircuitDiagramDeleteRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "unmarshal circuit diagram del info failed", "error", err)
|
logger.Error(c, "unmarshal circuit diagram del info failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -38,7 +37,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
|
||||||
|
|
||||||
graph, err := diagram.GetGraphMap(request.PageID)
|
graph, err := diagram.GetGraphMap(request.PageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
|
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -66,7 +65,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
|
||||||
err = fmt.Errorf("convert uuid from string failed:%w:%w", err1, err2)
|
err = fmt.Errorf("convert uuid from string failed:%w:%w", err1, err2)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Error(ctx, "format uuid from string failed", "error", err)
|
logger.Error(c, "format uuid from string failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -84,14 +83,14 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// open transaction
|
// open transaction
|
||||||
tx := pgClient.WithContext(ctx).Begin()
|
tx := pgClient.Begin()
|
||||||
|
|
||||||
for _, topologicDelInfo := range topologicDelInfos {
|
for _, topologicDelInfo := range topologicDelInfos {
|
||||||
err = database.DeleteTopologicIntoDB(ctx, tx, request.PageID, topologicDelInfo)
|
err = database.DeleteTopologicIntoDB(c, tx, request.PageID, topologicDelInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|
||||||
logger.Error(ctx, "delete topologic info into DB failed", "topologic_info", topologicDelInfo, "error", err)
|
logger.Error(c, "delete topologic info into DB failed", "topologic_info", topologicDelInfo, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -108,7 +107,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|
||||||
logger.Error(ctx, "delete topologic info failed", "topologic_info", topologicDelInfo, "error", err)
|
logger.Error(c, "delete topologic info failed", "topologic_info", topologicDelInfo, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -127,14 +126,14 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, componentInfo := range request.ComponentInfos {
|
for _, componentInfo := range request.ComponentInfos {
|
||||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
cancelCtx, cancel := context.WithTimeout(c, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
globalUUID, err := uuid.FromString(componentInfo.UUID)
|
globalUUID, err := uuid.FromString(componentInfo.UUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|
||||||
logger.Error(ctx, "format uuid from string failed", "error", err)
|
logger.Error(c, "format uuid from string failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -158,7 +157,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
|
||||||
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
|
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Error(ctx, "query component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
|
logger.Error(c, "query component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -180,7 +179,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
|
||||||
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
|
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Error(ctx, "delete component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
|
logger.Error(c, "delete component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,11 @@ import (
|
||||||
// @Failure 400 {object} network.FailureResponse "request process failed"
|
// @Failure 400 {object} network.FailureResponse "request process failed"
|
||||||
// @Router /model/diagram_load/{page_id} [get]
|
// @Router /model/diagram_load/{page_id} [get]
|
||||||
func CircuitDiagramLoadHandler(c *gin.Context) {
|
func CircuitDiagramLoadHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
|
|
||||||
pageID, err := strconv.ParseInt(c.Query("page_id"), 10, 64)
|
pageID, err := strconv.ParseInt(c.Query("page_id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get pageID from url param failed", "error", err)
|
logger.Error(c, "get pageID from url param failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -44,7 +43,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
|
||||||
|
|
||||||
topologicInfo, err := diagram.GetGraphMap(pageID)
|
topologicInfo, err := diagram.GetGraphMap(pageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
|
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -63,9 +62,9 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
|
||||||
componentParamMap := make(map[string]any)
|
componentParamMap := make(map[string]any)
|
||||||
for _, VerticeLink := range topologicInfo.VerticeLinks {
|
for _, VerticeLink := range topologicInfo.VerticeLinks {
|
||||||
for _, componentUUID := range VerticeLink {
|
for _, componentUUID := range VerticeLink {
|
||||||
component, err := database.QueryComponentByUUID(ctx, pgClient, componentUUID)
|
component, err := database.QueryComponentByUUID(c, pgClient, componentUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get component id info from DB by uuid failed", "error", err)
|
logger.Error(c, "get component id info from DB by uuid failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -80,7 +79,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
|
||||||
|
|
||||||
componentParams, err := diagram.GetComponentMap(component.GlobalUUID.String())
|
componentParams, err := diagram.GetComponentMap(component.GlobalUUID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get component data from set by uuid failed", "error", err)
|
logger.Error(c, "get component data from set by uuid failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -97,9 +96,9 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
rootVertexUUID := topologicInfo.RootVertex.String()
|
rootVertexUUID := topologicInfo.RootVertex.String()
|
||||||
rootComponent, err := database.QueryComponentByUUID(ctx, pgClient, topologicInfo.RootVertex)
|
rootComponent, err := database.QueryComponentByUUID(c, pgClient, topologicInfo.RootVertex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get component id info from DB by uuid failed", "error", err)
|
logger.Error(c, "get component id info from DB by uuid failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -114,7 +113,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
|
||||||
|
|
||||||
rootComponentParam, err := diagram.GetComponentMap(rootComponent.GlobalUUID.String())
|
rootComponentParam, err := diagram.GetComponentMap(rootComponent.GlobalUUID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get component data from set by uuid failed", "error", err)
|
logger.Error(c, "get component data from set by uuid failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,11 @@ import (
|
||||||
|
|
||||||
// CircuitDiagramUpdateHandler define circuit diagram update process API
|
// CircuitDiagramUpdateHandler define circuit diagram update process API
|
||||||
func CircuitDiagramUpdateHandler(c *gin.Context) {
|
func CircuitDiagramUpdateHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
|
|
||||||
var request network.CircuitDiagramUpdateRequest
|
var request network.CircuitDiagramUpdateRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "unmarshal circuit diagram update info failed", "error", err)
|
logger.Error(c, "unmarshal circuit diagram update info failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -31,7 +30,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
|
||||||
|
|
||||||
graph, err := diagram.GetGraphMap(request.PageID)
|
graph, err := diagram.GetGraphMap(request.PageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
|
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -48,7 +47,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
|
||||||
for _, topologicLink := range request.TopologicLinks {
|
for _, topologicLink := range request.TopologicLinks {
|
||||||
changeInfo, err := network.ParseUUID(topologicLink)
|
changeInfo, err := network.ParseUUID(topologicLink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "format uuid from string failed", "error", err)
|
logger.Error(c, "format uuid from string failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -64,14 +63,14 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// open transaction
|
// open transaction
|
||||||
tx := pgClient.WithContext(ctx).Begin()
|
tx := pgClient.Begin()
|
||||||
|
|
||||||
for _, topologicChangeInfo := range topologicChangeInfos {
|
for _, topologicChangeInfo := range topologicChangeInfos {
|
||||||
err = database.UpdateTopologicIntoDB(ctx, tx, request.PageID, topologicChangeInfo)
|
err = database.UpdateTopologicIntoDB(c, tx, request.PageID, topologicChangeInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|
||||||
logger.Error(ctx, "update topologic info into DB failed", "topologic_info", topologicChangeInfo, "error", err)
|
logger.Error(c, "update topologic info into DB failed", "topologic_info", topologicChangeInfo, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -88,7 +87,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
|
||||||
logger.Error(ctx, "update topologic info failed", "topologic_info", topologicChangeInfo, "error", err)
|
logger.Error(c, "update topologic info failed", "topologic_info", topologicChangeInfo, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -103,9 +102,9 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for index, componentInfo := range request.ComponentInfos {
|
for index, componentInfo := range request.ComponentInfos {
|
||||||
componentUUID, err := database.UpdateComponentIntoDB(ctx, tx, componentInfo)
|
componentUUID, err := database.UpdateComponentIntoDB(c, tx, componentInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "udpate component info into DB failed", "error", err)
|
logger.Error(c, "udpate component info into DB failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -125,7 +124,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
|
||||||
// TODO 修复赋值问题
|
// TODO 修复赋值问题
|
||||||
component, err := network.ConvertComponentUpdateInfosToComponents(info)
|
component, err := network.ConvertComponentUpdateInfosToComponents(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "convert component params info failed", "component_info", info, "error", err)
|
logger.Error(c, "convert component params info failed", "component_info", info, "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
|
||||||
|
|
||||||
"modelRT/common/errcode"
|
"modelRT/common/errcode"
|
||||||
"modelRT/constants"
|
"modelRT/constants"
|
||||||
"modelRT/database"
|
"modelRT/database"
|
||||||
|
|
@ -18,17 +16,17 @@ import (
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ComponentAttributeQueryHandler define circuit diagram component attribute value query process API
|
// ComponentAttributeQueryHandler define circuit diagram component attribute value query process API
|
||||||
func ComponentAttributeQueryHandler(c *gin.Context) {
|
func ComponentAttributeQueryHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
|
|
||||||
tokens := c.Param("tokens")
|
tokens := c.Param("tokens")
|
||||||
if tokens == "" {
|
if tokens == "" {
|
||||||
err := fmt.Errorf("tokens is missing from the path")
|
err := fmt.Errorf("tokens is missing from the path")
|
||||||
logger.Error(ctx, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
|
logger.Error(c, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -55,10 +53,10 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
|
||||||
dbQueryMap := make(map[string][]cacheQueryItem)
|
dbQueryMap := make(map[string][]cacheQueryItem)
|
||||||
var secondaryQueryCount int
|
var secondaryQueryCount int
|
||||||
for hSetKey, items := range cacheQueryMap {
|
for hSetKey, items := range cacheQueryMap {
|
||||||
hset := diagram.NewRedisHash(ctx, hSetKey, 5000, false)
|
hset := diagram.NewRedisHash(c, hSetKey, 5000, false)
|
||||||
cacheData, err := hset.HGetAll()
|
cacheData, err := hset.HGetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(ctx, "redis hgetall failed", "key", hSetKey, "err", err)
|
logger.Warn(c, "redis hgetall failed", "key", hSetKey, "err", err)
|
||||||
}
|
}
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if val, ok := cacheData[item.attributeName]; ok {
|
if val, ok := cacheData[item.attributeName]; ok {
|
||||||
|
|
@ -76,9 +74,9 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tx := pgClient.WithContext(ctx).Begin()
|
tx := pgClient.WithContext(c).Begin()
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
logger.Error(ctx, "begin postgres transaction failed", "error", tx.Error)
|
logger.Error(c, "begin postgres transaction failed", "error", tx.Error)
|
||||||
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrBeginTxFailed)
|
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrBeginTxFailed)
|
||||||
payload := genQueryRespPayload(queryResults, tokenSlice)
|
payload := genQueryRespPayload(queryResults, tokenSlice)
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "begin postgres database transaction failed", payload)
|
renderRespFailure(c, constants.RespCodeServerError, "begin postgres database transaction failed", payload)
|
||||||
|
|
@ -87,9 +85,9 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
allCompTags := slices.Collect(maps.Keys(dbQueryMap))
|
allCompTags := slices.Collect(maps.Keys(dbQueryMap))
|
||||||
compModelMap, err := database.QueryComponentByCompTags(ctx, tx, allCompTags)
|
compModelMap, err := database.QueryComponentByCompTags(c, tx, allCompTags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "query component info from postgres database failed", "error", err)
|
logger.Error(c, "query component info from postgres database failed", "error", err)
|
||||||
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrDBQueryFailed)
|
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrDBQueryFailed)
|
||||||
payload := genQueryRespPayload(queryResults, tokenSlice)
|
payload := genQueryRespPayload(queryResults, tokenSlice)
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "query component meta failed", payload)
|
renderRespFailure(c, constants.RespCodeServerError, "query component meta failed", payload)
|
||||||
|
|
@ -117,7 +115,7 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
|
||||||
|
|
||||||
tableNameMap, err := database.BatchGetProjectNames(tx, identifiers)
|
tableNameMap, err := database.BatchGetProjectNames(tx, identifiers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "batch get table names from postgres database failed", "error", err)
|
logger.Error(c, "batch get table names from postgres database failed", "error", err)
|
||||||
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrRetrieveFailed)
|
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrRetrieveFailed)
|
||||||
payload := genQueryRespPayload(queryResults, tokenSlice)
|
payload := genQueryRespPayload(queryResults, tokenSlice)
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "batch get table names from postgres database failed", payload)
|
renderRespFailure(c, constants.RespCodeServerError, "batch get table names from postgres database failed", payload)
|
||||||
|
|
@ -152,11 +150,10 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
if err := tx.Commit().Error; err != nil {
|
||||||
logger.Warn(ctx, "postgres transaction commit failed, but returning scanned data", "error", err)
|
logger.Warn(c, "postgres transaction commit failed, but returning scanned data", "error", err)
|
||||||
} else {
|
} else {
|
||||||
backfillCtx := context.WithoutCancel(ctx)
|
|
||||||
for hKey, items := range redisSyncMap {
|
for hKey, items := range redisSyncMap {
|
||||||
go backfillRedis(backfillCtx, hKey, items)
|
go backfillRedis(c.Copy(), hKey, items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,10 @@ import (
|
||||||
|
|
||||||
// ComponentAttributeUpdateHandler define circuit diagram component attribute value update process API
|
// ComponentAttributeUpdateHandler define circuit diagram component attribute value update process API
|
||||||
func ComponentAttributeUpdateHandler(c *gin.Context) {
|
func ComponentAttributeUpdateHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
var request network.ComponentAttributeUpdateInfo
|
var request network.ComponentAttributeUpdateInfo
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "unmarshal request params failed", "error", err)
|
logger.Error(c, "unmarshal request params failed", "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -55,16 +54,16 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// open transaction
|
// open transaction
|
||||||
tx := pgClient.WithContext(ctx).Begin()
|
tx := pgClient.WithContext(c).Begin()
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
logger.Error(ctx, "begin postgres transaction failed", "error", tx.Error)
|
logger.Error(c, "begin postgres transaction failed", "error", tx.Error)
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "begin postgres transaction failed", nil)
|
renderRespFailure(c, constants.RespCodeServerError, "begin postgres transaction failed", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
compInfo, err := database.QueryComponentByCompTag(ctx, tx, attributeComponentTag)
|
compInfo, err := database.QueryComponentByCompTag(c, tx, attributeComponentTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "query component info by component tag failed", "error", err, "tag", attributeComponentTag)
|
logger.Error(c, "query component info by component tag failed", "error", err, "tag", attributeComponentTag)
|
||||||
|
|
||||||
for _, attribute := range request.AttributeConfigs {
|
for _, attribute := range request.AttributeConfigs {
|
||||||
if _, exists := updateResults[attribute.AttributeToken]; !exists {
|
if _, exists := updateResults[attribute.AttributeToken]; !exists {
|
||||||
|
|
@ -140,7 +139,7 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, items := range redisUpdateMap {
|
for key, items := range redisUpdateMap {
|
||||||
hset := diagram.NewRedisHash(ctx, key, 5000, false)
|
hset := diagram.NewRedisHash(c, key, 5000, false)
|
||||||
|
|
||||||
fields := make(map[string]any, len(items))
|
fields := make(map[string]any, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
|
|
@ -148,7 +147,7 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := hset.SetRedisHashByMap(fields); err != nil {
|
if err := hset.SetRedisHashByMap(fields); err != nil {
|
||||||
logger.Error(ctx, "batch sync redis failed", "hash_key", key, "error", err)
|
logger.Error(c, "batch sync redis failed", "hash_key", key, "error", err)
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if _, exists := updateResults[item.token]; exists {
|
if _, exists := updateResults[item.token]; exists {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
// Package handler provides HTTP handlers for various endpoints.
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"modelRT/common"
|
||||||
|
"modelRT/common/errcode"
|
||||||
|
"modelRT/constants"
|
||||||
|
"modelRT/database"
|
||||||
|
"modelRT/diagram"
|
||||||
|
"modelRT/logger"
|
||||||
|
"modelRT/model"
|
||||||
|
"modelRT/orm"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataObjectAttributeQueryHandler define data object attribute value query process API
|
||||||
|
func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
pgClient := database.GetPostgresDBClient()
|
||||||
|
|
||||||
|
token := c.Param("token")
|
||||||
|
field := c.Param("field")
|
||||||
|
if token == "" {
|
||||||
|
err := fmt.Errorf("tokens is missing from the path")
|
||||||
|
logger.Error(ctx, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
|
||||||
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
|
return
|
||||||
|
} else if field == "" {
|
||||||
|
err := fmt.Errorf("field is missing from the path")
|
||||||
|
logger.Error(ctx, "query field from path failed", "error", err, "url", c.Request.RequestURI)
|
||||||
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO 根据传入的token查询是measurement还是attribute,分别查询不同的表
|
||||||
|
// 量测支持token1.token2.token3.token4.token5.token6.token7、token4.token5.token6.token7、token4.token7
|
||||||
|
// 参量支持两种形式token4.token5.token6.token7与token1.token2.token3.token4.token5.token6.token7
|
||||||
|
dataObjectType, err := model.ClassifyDataObjectToken(token)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "classify data object token failed", "error", err, "url", c.Request.RequestURI)
|
||||||
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var measurement *orm.Measurement
|
||||||
|
var measurementComponent *orm.Component
|
||||||
|
switch dataObjectType {
|
||||||
|
case constants.DataObjectTypeParameter:
|
||||||
|
// TODO 验证参量token的合法性
|
||||||
|
case constants.DataObjectTypeMeasurement:
|
||||||
|
measurement, measurementComponent, err = database.QueryMeasurementByDataObjectToken(ctx, pgClient, token)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, common.ErrInvalidMeasurementToken) ||
|
||||||
|
errors.Is(err, common.ErrMeasurementTokenNotFound) ||
|
||||||
|
errors.Is(err, common.ErrAmbiguousMeasurementToken) {
|
||||||
|
logger.Warn(ctx, "validate measurement token failed", "token", token, "error", err)
|
||||||
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(ctx, "query measurement token from postgres failed", "token", token, "error", err)
|
||||||
|
renderRespFailure(c, constants.RespCodeServerError, "validate measurement token failed", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dataObjectType {
|
||||||
|
case constants.DataObjectTypeParameter:
|
||||||
|
// TODO 在根据传递的field来组装具体的查询结果
|
||||||
|
// 目前先直接返回错误,后续再实现具体的查询逻辑
|
||||||
|
renderRespFailure(c, constants.RespCodeServerError, "query parameter attribute is not implemented yet", nil)
|
||||||
|
case constants.DataObjectTypeMeasurement:
|
||||||
|
normalizedField := strings.ToLower(field)
|
||||||
|
value, err := buildMeasurementAttributeValue(
|
||||||
|
ctx,
|
||||||
|
normalizedField,
|
||||||
|
measurement,
|
||||||
|
measurementComponent,
|
||||||
|
queryMeasurementRealtimeValue,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, common.ErrUnsupportedMeasurementField) {
|
||||||
|
logger.Warn(ctx, "query unsupported measurement field", "token", token, "field", field, "error", err)
|
||||||
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(ctx, "build measurement attribute value failed", "token", token, "field", field, "error", err)
|
||||||
|
renderRespFailure(c, constants.RespCodeServerError, "query measurement attribute failed", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := dataObjectAttributeQueryResult{
|
||||||
|
Token: token,
|
||||||
|
Field: normalizedField,
|
||||||
|
Code: errcode.ErrProcessSuccess.Code(),
|
||||||
|
Msg: errcode.ErrProcessSuccess.Msg(),
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
renderRespSuccess(c, constants.RespCodeSuccess, "query measurement attribute success", map[string]any{
|
||||||
|
"attributes": []dataObjectAttributeQueryResult{result},
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
renderRespFailure(c, constants.RespCodeInvalidParams, "invalid data object type", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type measurementValueLoader func(context.Context, orm.JSONMap) (any, error)
|
||||||
|
|
||||||
|
type dataObjectAttributeQueryResult struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Field string `json:"field"`
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Value any `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMeasurementAttributeValue(
|
||||||
|
ctx context.Context,
|
||||||
|
field string,
|
||||||
|
measurement *orm.Measurement,
|
||||||
|
component *orm.Component,
|
||||||
|
loadValue measurementValueLoader,
|
||||||
|
) (any, error) {
|
||||||
|
if measurement == nil {
|
||||||
|
return nil, fmt.Errorf("measurement is nil")
|
||||||
|
}
|
||||||
|
if component == nil {
|
||||||
|
return nil, fmt.Errorf("measurement component is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field {
|
||||||
|
case "value":
|
||||||
|
if loadValue == nil {
|
||||||
|
return nil, fmt.Errorf("measurement value loader is nil")
|
||||||
|
}
|
||||||
|
return loadValue(ctx, measurement.DataSource)
|
||||||
|
case "mode":
|
||||||
|
fmt.Println("measurement.Mode:", measurement.Mode)
|
||||||
|
switch measurement.Mode {
|
||||||
|
case 0:
|
||||||
|
return false, nil
|
||||||
|
case 1:
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid measurement mode %d: expected 0 or 1", measurement.Mode)
|
||||||
|
}
|
||||||
|
case "meta":
|
||||||
|
return "MEASUREMENT", nil
|
||||||
|
case "type":
|
||||||
|
return model.MeasurementTypeFromDataSource(measurement.DataSource)
|
||||||
|
case "name":
|
||||||
|
// The resolved measurement and component prove that token4.token7 exists.
|
||||||
|
return component.NSPath + "." + measurement.Tag, nil
|
||||||
|
case "description":
|
||||||
|
return measurement.Name, nil
|
||||||
|
case "id":
|
||||||
|
return strings.Join([]string{
|
||||||
|
component.GridName,
|
||||||
|
component.ZoneName,
|
||||||
|
component.StationName,
|
||||||
|
component.NSPath,
|
||||||
|
component.Tag,
|
||||||
|
"bay",
|
||||||
|
measurement.Tag,
|
||||||
|
}, "."), nil
|
||||||
|
case "size":
|
||||||
|
return measurement.Size, nil
|
||||||
|
case "data_source":
|
||||||
|
return measurement.DataSource, nil
|
||||||
|
case "event_plan":
|
||||||
|
return measurement.EventPlan, nil
|
||||||
|
case "binding":
|
||||||
|
return measurement.Binding, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: %s", common.ErrUnsupportedMeasurementField, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryMeasurementRealtimeValue(ctx context.Context, dataSource orm.JSONMap) (any, error) {
|
||||||
|
queryKey, err := model.GenerateMeasureIdentifier(dataSource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate measurement redis key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := diagram.NewRedisClient().QueryLatestMeasurementValue(ctx, queryKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query real-time measurement value by key %q: %w", queryKey, err)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"modelRT/common"
|
||||||
|
"modelRT/orm"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildMeasurementAttributeValue(t *testing.T) {
|
||||||
|
dataSource := orm.JSONMap{
|
||||||
|
"type": float64(1),
|
||||||
|
"io_address": map[string]any{
|
||||||
|
"channel": "tm1p",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
eventPlan := orm.JSONMap{"enabled": true}
|
||||||
|
binding := orm.JSONMap{"ct": map[string]any{"ratio": float64(2)}}
|
||||||
|
measurement := &orm.Measurement{
|
||||||
|
Tag: "IA_rms",
|
||||||
|
Name: "A相电流",
|
||||||
|
Mode: 1,
|
||||||
|
Size: 10,
|
||||||
|
DataSource: dataSource,
|
||||||
|
EventPlan: eventPlan,
|
||||||
|
Binding: binding,
|
||||||
|
}
|
||||||
|
component := &orm.Component{
|
||||||
|
GridName: "grid000",
|
||||||
|
ZoneName: "zone000",
|
||||||
|
StationName: "station000",
|
||||||
|
NSPath: "110kV_TV",
|
||||||
|
Tag: "cable_22",
|
||||||
|
}
|
||||||
|
|
||||||
|
loader := func(_ context.Context, source orm.JSONMap) (any, error) {
|
||||||
|
assert.Equal(t, dataSource, source)
|
||||||
|
return float64(220), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
field string
|
||||||
|
expected any
|
||||||
|
}{
|
||||||
|
{field: "value", expected: float64(220)},
|
||||||
|
{field: "mode", expected: true},
|
||||||
|
{field: "meta", expected: "MEASUREMENT"},
|
||||||
|
{field: "type", expected: "TM"},
|
||||||
|
{field: "name", expected: "110kV_TV.IA_rms"},
|
||||||
|
{field: "description", expected: "A相电流"},
|
||||||
|
{field: "id", expected: "grid000.zone000.station000.110kV_TV.cable_22.bay.IA_rms"},
|
||||||
|
{field: "size", expected: 10},
|
||||||
|
{field: "data_source", expected: dataSource},
|
||||||
|
{field: "event_plan", expected: eventPlan},
|
||||||
|
{field: "binding", expected: binding},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.field, func(t *testing.T) {
|
||||||
|
actual, err := buildMeasurementAttributeValue(context.Background(), tt.field, measurement, component, loader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMeasurementAttributeValueRejectsUnsupportedField(t *testing.T) {
|
||||||
|
_, err := buildMeasurementAttributeValue(
|
||||||
|
context.Background(),
|
||||||
|
"unknown",
|
||||||
|
&orm.Measurement{},
|
||||||
|
&orm.Component{},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, common.ErrUnsupportedMeasurementField)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMeasurementAttributeValueMode(t *testing.T) {
|
||||||
|
component := &orm.Component{}
|
||||||
|
|
||||||
|
manualMode, err := buildMeasurementAttributeValue(
|
||||||
|
context.Background(),
|
||||||
|
"mode",
|
||||||
|
&orm.Measurement{Mode: 0},
|
||||||
|
component,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, false, manualMode)
|
||||||
|
|
||||||
|
automaticMode, err := buildMeasurementAttributeValue(
|
||||||
|
context.Background(),
|
||||||
|
"mode",
|
||||||
|
&orm.Measurement{Mode: 1},
|
||||||
|
component,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, true, automaticMode)
|
||||||
|
|
||||||
|
_, err = buildMeasurementAttributeValue(
|
||||||
|
context.Background(),
|
||||||
|
"mode",
|
||||||
|
&orm.Measurement{Mode: 2},
|
||||||
|
component,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "expected 0 or 1")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
// Package handler provides HTTP handlers for various endpoints.
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"modelRT/common/errcode"
|
||||||
|
"modelRT/constants"
|
||||||
|
"modelRT/database"
|
||||||
|
"modelRT/diagram"
|
||||||
|
"modelRT/logger"
|
||||||
|
"modelRT/network"
|
||||||
|
"modelRT/orm"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataObjectAttributeUpdateHandler define data object attribute value update process API
|
||||||
|
func DataObjectAttributeUpdateHandler(c *gin.Context) {
|
||||||
|
pgClient := database.GetPostgresDBClient()
|
||||||
|
var request network.ComponentAttributeUpdateInfo
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
logger.Error(c, "unmarshal request params failed", "error", err)
|
||||||
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResults := make(map[string]*errcode.AppError)
|
||||||
|
attriModifyConfs := make([]attributeModifyConfig, 0, len(request.AttributeConfigs))
|
||||||
|
var attributeComponentTag string
|
||||||
|
for index, attribute := range request.AttributeConfigs {
|
||||||
|
slices := strings.Split(attribute.AttributeToken, ".")
|
||||||
|
if len(slices) < 7 {
|
||||||
|
updateResults[attribute.AttributeToken] = errcode.ErrInvalidToken
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
componentTag := slices[4]
|
||||||
|
if index == 0 {
|
||||||
|
attributeComponentTag = componentTag
|
||||||
|
} else if componentTag != attributeComponentTag {
|
||||||
|
updateResults[attribute.AttributeToken] = errcode.ErrCrossToken
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attriModifyConfs = append(attriModifyConfs, attributeModifyConfig{
|
||||||
|
attributeToken: attribute.AttributeToken,
|
||||||
|
attributeExtendType: slices[5],
|
||||||
|
attributeName: slices[6],
|
||||||
|
attributeOldVal: attribute.AttributeOldVal,
|
||||||
|
attributeNewVal: attribute.AttributeNewVal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// open transaction
|
||||||
|
tx := pgClient.WithContext(c).Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
logger.Error(c, "begin postgres transaction failed", "error", tx.Error)
|
||||||
|
renderRespFailure(c, constants.RespCodeServerError, "begin postgres transaction failed", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
compInfo, err := database.QueryComponentByCompTag(c, tx, attributeComponentTag)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(c, "query component info by component tag failed", "error", err, "tag", attributeComponentTag)
|
||||||
|
|
||||||
|
for _, attribute := range request.AttributeConfigs {
|
||||||
|
if _, exists := updateResults[attribute.AttributeToken]; !exists {
|
||||||
|
updateResults[attribute.AttributeToken] = errcode.ErrDBQueryFailed.WithCause(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Rollback()
|
||||||
|
|
||||||
|
payload := genUpdateRespPayload(updateResults, request.AttributeConfigs)
|
||||||
|
renderRespFailure(c, constants.RespCodeFailed, "query component metadata failed", payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identifiers := make([]orm.ProjectIdentifier, len(attriModifyConfs))
|
||||||
|
for i, mod := range attriModifyConfs {
|
||||||
|
identifiers[i] = orm.ProjectIdentifier{
|
||||||
|
Token: mod.attributeToken,
|
||||||
|
Tag: compInfo.ModelName,
|
||||||
|
GroupName: mod.attributeExtendType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tableNameMap, err := database.BatchGetProjectNames(tx, identifiers)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
|
||||||
|
for _, id := range identifiers {
|
||||||
|
if _, exists := updateResults[id.Token]; !exists {
|
||||||
|
updateResults[id.Token] = errcode.ErrRetrieveFailed.WithCause(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := genUpdateRespPayload(updateResults, request.AttributeConfigs)
|
||||||
|
renderRespFailure(c, constants.RespCodeFailed, "batch retrieve table names failed", payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redisUpdateMap := make(map[string][]cacheUpdateItem)
|
||||||
|
for _, mod := range attriModifyConfs {
|
||||||
|
id := orm.ProjectIdentifier{Tag: compInfo.ModelName, GroupName: mod.attributeExtendType}
|
||||||
|
tableName, exists := tableNameMap[id]
|
||||||
|
if !exists {
|
||||||
|
updateResults[mod.attributeToken] = errcode.ErrFoundTargetFailed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result := tx.Table(tableName).
|
||||||
|
Where(fmt.Sprintf("%s = ? AND global_uuid = ?", mod.attributeName), mod.attributeOldVal, compInfo.GlobalUUID).
|
||||||
|
Updates(map[string]any{mod.attributeName: mod.attributeNewVal})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
updateResults[mod.attributeToken] = errcode.ErrDBUpdateFailed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
updateResults[mod.attributeToken] = errcode.ErrDBzeroAffectedRows
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("%s_%s", attributeComponentTag, mod.attributeExtendType)
|
||||||
|
redisUpdateMap[cacheKey] = append(redisUpdateMap[cacheKey],
|
||||||
|
cacheUpdateItem{
|
||||||
|
token: mod.attributeToken,
|
||||||
|
name: mod.attributeName,
|
||||||
|
newVal: mod.attributeNewVal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// commit transaction
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
renderRespFailure(c, constants.RespCodeServerError, "transaction commit failed", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, items := range redisUpdateMap {
|
||||||
|
hset := diagram.NewRedisHash(c, key, 5000, false)
|
||||||
|
|
||||||
|
fields := make(map[string]any, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
fields[item.name] = item.newVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hset.SetRedisHashByMap(fields); err != nil {
|
||||||
|
logger.Error(c, "batch sync redis failed", "hash_key", key, "error", err)
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
if _, exists := updateResults[item.token]; exists {
|
||||||
|
updateResults[item.token] = errcode.ErrCacheSyncWarn.WithCause(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := genUpdateRespPayload(updateResults, request.AttributeConfigs)
|
||||||
|
if len(updateResults) > 0 {
|
||||||
|
renderRespFailure(c, constants.RespCodeFailed, "process completed with partial failures", payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderRespSuccess(c, constants.RespCodeSuccess, "process completed successfully", payload)
|
||||||
|
}
|
||||||
|
|
@ -41,12 +41,11 @@ var linkSetConfigs = map[int]linkSetConfig{
|
||||||
|
|
||||||
// DiagramNodeLinkHandler defines the diagram node link process api
|
// DiagramNodeLinkHandler defines the diagram node link process api
|
||||||
func DiagramNodeLinkHandler(c *gin.Context) {
|
func DiagramNodeLinkHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.DiagramNodeLinkRequest
|
var request network.DiagramNodeLinkRequest
|
||||||
clientToken := c.GetString("client_token")
|
clientToken := c.GetString("client_token")
|
||||||
if clientToken == "" {
|
if clientToken == "" {
|
||||||
err := common.ErrGetClientToken
|
err := common.ErrGetClientToken
|
||||||
logger.Error(ctx, "failed to get client token from context", "error", err)
|
logger.Error(c, "failed to get client token from context", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -55,7 +54,7 @@ func DiagramNodeLinkHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to unmarshal diagram node process request", "error", err)
|
logger.Error(c, "failed to unmarshal diagram node process request", "error", err)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -69,9 +68,9 @@ func DiagramNodeLinkHandler(c *gin.Context) {
|
||||||
nodeID := request.NodeID
|
nodeID := request.NodeID
|
||||||
nodeLevel := request.NodeLevel
|
nodeLevel := request.NodeLevel
|
||||||
action := request.Action
|
action := request.Action
|
||||||
prevNodeInfo, currNodeInfo, err := database.QueryNodeInfoByID(ctx, pgClient, nodeID, nodeLevel)
|
prevNodeInfo, currNodeInfo, err := database.QueryNodeInfoByID(c, pgClient, nodeID, nodeLevel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "failed to query diagram node info by nodeID and level from postgres", "node_id", nodeID, "level", nodeLevel, "error", err)
|
logger.Error(c, "failed to query diagram node info by nodeID and level from postgres", "node_id", nodeID, "level", nodeLevel, "error", err)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -85,8 +84,8 @@ func DiagramNodeLinkHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
prevLinkSet, currLinkSet := generateLinkSet(ctx, nodeLevel, prevNodeInfo)
|
prevLinkSet, currLinkSet := generateLinkSet(c, nodeLevel, prevNodeInfo)
|
||||||
err = processLinkSetData(ctx, action, nodeLevel, prevLinkSet, currLinkSet, prevNodeInfo, currNodeInfo)
|
err = processLinkSetData(c, action, nodeLevel, prevLinkSet, currLinkSet, prevNodeInfo, currNodeInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -100,7 +99,7 @@ func DiagramNodeLinkHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(ctx, "process diagram node link success", "node_id", nodeID, "level", nodeLevel, "action", request.Action)
|
logger.Info(c, "process diagram node link success", "node_id", nodeID, "level", nodeLevel, "action", request.Action)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, network.SuccessResponse{
|
c.JSON(http.StatusOK, network.SuccessResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,11 @@ import (
|
||||||
|
|
||||||
// QueryHistoryDataHandler define query history data process API
|
// QueryHistoryDataHandler define query history data process API
|
||||||
func QueryHistoryDataHandler(c *gin.Context) {
|
func QueryHistoryDataHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
token := c.Query("token")
|
token := c.Query("token")
|
||||||
beginStr := c.Query("begin")
|
beginStr := c.Query("begin")
|
||||||
begin, err := strconv.Atoi(beginStr)
|
begin, err := strconv.Atoi(beginStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "convert begin param from string to int failed", "error", err)
|
logger.Error(c, "convert begin param from string to int failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -32,7 +31,7 @@ func QueryHistoryDataHandler(c *gin.Context) {
|
||||||
endStr := c.Query("end")
|
endStr := c.Query("end")
|
||||||
end, err := strconv.Atoi(endStr)
|
end, err := strconv.Atoi(endStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "convert end param from string to int failed", "error", err)
|
logger.Error(c, "convert end param from string to int failed", "error", err)
|
||||||
|
|
||||||
resp := network.FailureResponse{
|
resp := network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,13 @@ import (
|
||||||
|
|
||||||
// MeasurementGetHandler define measurement query API
|
// MeasurementGetHandler define measurement query API
|
||||||
func MeasurementGetHandler(c *gin.Context) {
|
func MeasurementGetHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.MeasurementGetRequest
|
var request network.MeasurementGetRequest
|
||||||
|
|
||||||
clientToken := c.GetString("client_token")
|
clientToken := c.GetString("client_token")
|
||||||
if clientToken == "" {
|
if clientToken == "" {
|
||||||
err := common.ErrGetClientToken
|
err := common.ErrGetClientToken
|
||||||
|
|
||||||
logger.Error(ctx, "failed to get client token from context", "error", err)
|
logger.Error(c, "failed to get client token from context", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -31,7 +30,7 @@ func MeasurementGetHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to unmarshal measurement get request", "error", err)
|
logger.Error(c, "failed to unmarshal measurement get request", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -39,10 +38,10 @@ func MeasurementGetHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
zset := diagram.NewRedisZSet(ctx, request.MeasurementToken, 0, false)
|
zset := diagram.NewRedisZSet(c, request.MeasurementToken, 0, false)
|
||||||
points, err := zset.ZRANGE(request.MeasurementToken, 0, -1)
|
points, err := zset.ZRANGE(request.MeasurementToken, 0, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "failed to get measurement data from redis", "measurement_token", request.MeasurementToken, "error", err)
|
logger.Error(c, "failed to get measurement data from redis", "measurement_token", request.MeasurementToken, "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -55,9 +54,9 @@ func MeasurementGetHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
measurementInfo, err := database.QueryMeasurementByID(ctx, pgClient, request.MeasurementID)
|
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, request.MeasurementID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "failed to query measurement by id", "measurement_id", request.MeasurementID, "error", err)
|
logger.Error(c, "failed to query measurement by id", "measurement_id", request.MeasurementID, "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"modelRT/constants"
|
|
||||||
"modelRT/logger"
|
"modelRT/logger"
|
||||||
"modelRT/model"
|
"modelRT/model"
|
||||||
"modelRT/network"
|
"modelRT/network"
|
||||||
|
"modelRT/util"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
@ -45,81 +43,79 @@ import (
|
||||||
//
|
//
|
||||||
// @Router /measurement/recommend [get]
|
// @Router /measurement/recommend [get]
|
||||||
func MeasurementRecommendHandler(c *gin.Context) {
|
func MeasurementRecommendHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.MeasurementRecommendRequest
|
var request network.MeasurementRecommendRequest
|
||||||
|
|
||||||
if err := c.ShouldBindQuery(&request); err != nil {
|
if err := c.ShouldBindQuery(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to bind measurement recommend request", "error", err)
|
logger.Error(c, "failed to bind measurement recommend request", "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
return
|
Code: http.StatusBadRequest,
|
||||||
}
|
Msg: err.Error(),
|
||||||
if err := validateMeasurementRecommendInput(request.Input); err != nil {
|
|
||||||
logger.Warn(ctx, "invalid measurement recommend input", "input", request.Input, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), map[string]any{
|
|
||||||
"input": request.Input,
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
recommendResults := model.RedisSearchRecommend(ctx, request.Input)
|
|
||||||
payload := network.MeasurementRecommendPayload{
|
|
||||||
Input: request.Input,
|
|
||||||
RecommendedList: make([]string, 0),
|
|
||||||
}
|
|
||||||
seen := make(map[string]struct{})
|
|
||||||
orderedResults := orderedRecommendResults(recommendResults)
|
|
||||||
|
|
||||||
for _, recommendResult := range orderedResults {
|
recommendResults := model.RedisSearchRecommend(c, request.Input)
|
||||||
|
payloads := make([]network.MeasurementRecommendPayload, 0, len(recommendResults))
|
||||||
|
for _, recommendResult := range recommendResults {
|
||||||
if recommendResult.Err != nil {
|
if recommendResult.Err != nil {
|
||||||
err := recommendResult.Err
|
err := recommendResult.Err
|
||||||
logger.Error(ctx, "failed to get recommend data from redis", "input", request.Input, "error", err)
|
logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeServerError, err.Error(), map[string]any{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
"input": request.Input,
|
Code: http.StatusInternalServerError,
|
||||||
|
Msg: err.Error(),
|
||||||
|
Payload: map[string]any{
|
||||||
|
"input": request.Input,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if recommendResult.Offset > payload.Offset {
|
var finalOffset int
|
||||||
payload.Offset = recommendResult.Offset
|
recommends := recommendResult.QueryDatas
|
||||||
|
if recommendResult.IsFuzzy {
|
||||||
|
var maxOffset int
|
||||||
|
for index, recommend := range recommends {
|
||||||
|
offset := util.GetLongestCommonPrefixLength(request.Input, recommend)
|
||||||
|
if index == 0 || offset > maxOffset {
|
||||||
|
maxOffset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalOffset = maxOffset
|
||||||
|
} else {
|
||||||
|
var minOffset int
|
||||||
|
for index, recommend := range recommends {
|
||||||
|
offset := util.GetLongestCommonPrefixLength(request.Input, recommend)
|
||||||
|
if index == 0 || offset < minOffset {
|
||||||
|
minOffset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalOffset = minOffset
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for _, recommendResult := range orderedResults {
|
resultRecommends := make([]string, 0, len(recommends))
|
||||||
for _, recommend := range recommendResult.QueryDatas {
|
seen := make(map[string]struct{})
|
||||||
if _, exists := seen[recommend]; !exists {
|
|
||||||
seen[recommend] = struct{}{}
|
for _, recommend := range recommends {
|
||||||
payload.RecommendedList = append(payload.RecommendedList, recommend)
|
recommendTerm := recommend[finalOffset:]
|
||||||
|
if len(recommendTerm) != 0 {
|
||||||
|
if _, exists := seen[recommendTerm]; !exists {
|
||||||
|
seen[recommendTerm] = struct{}{}
|
||||||
|
resultRecommends = append(resultRecommends, recommendTerm)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payloads = append(payloads, network.MeasurementRecommendPayload{
|
||||||
|
Input: request.Input,
|
||||||
|
Offset: finalOffset,
|
||||||
|
RecommendType: recommendResult.RecommendType.String(),
|
||||||
|
RecommendedList: resultRecommends,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRespSuccess(c, constants.RespCodeSuccess, "success", payload)
|
c.JSON(http.StatusOK, network.SuccessResponse{
|
||||||
}
|
Code: http.StatusOK,
|
||||||
|
Msg: "success",
|
||||||
func orderedRecommendResults(recommendResults map[string]model.SearchResult) []model.SearchResult {
|
Payload: &payloads,
|
||||||
orderedTypes := []string{
|
})
|
||||||
constants.CompNSPathRecommendHierarchyType.String(),
|
|
||||||
constants.GridRecommendHierarchyType.String(),
|
|
||||||
constants.ZoneRecommendHierarchyType.String(),
|
|
||||||
constants.StationRecommendHierarchyType.String(),
|
|
||||||
constants.CompTagRecommendHierarchyType.String(),
|
|
||||||
constants.ConfigRecommendHierarchyType.String(),
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make([]model.SearchResult, 0, len(recommendResults))
|
|
||||||
for _, recommendType := range orderedTypes {
|
|
||||||
result, ok := recommendResults[recommendType]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
results = append(results, result)
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateMeasurementRecommendInput(input string) error {
|
|
||||||
if strings.Contains(input, "..") {
|
|
||||||
return errors.New("input contains continuous dots")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestValidateMeasurementRecommendInput(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
valid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "continuous dots",
|
|
||||||
input: "G..",
|
|
||||||
valid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single separator",
|
|
||||||
input: "G.zone",
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "trailing dot",
|
|
||||||
input: "G.",
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty input",
|
|
||||||
input: "",
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
err := validateMeasurementRecommendInput(tt.input)
|
|
||||||
if tt.valid && err != nil {
|
|
||||||
t.Fatalf("expected valid input, got error %v", err)
|
|
||||||
}
|
|
||||||
if !tt.valid && err == nil {
|
|
||||||
t.Fatalf("expected invalid input")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,12 +18,11 @@ import (
|
||||||
|
|
||||||
// MeasurementLinkHandler defines the measurement link process api
|
// MeasurementLinkHandler defines the measurement link process api
|
||||||
func MeasurementLinkHandler(c *gin.Context) {
|
func MeasurementLinkHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.MeasurementLinkRequest
|
var request network.MeasurementLinkRequest
|
||||||
clientToken := c.GetString("client_token")
|
clientToken := c.GetString("client_token")
|
||||||
if clientToken == "" {
|
if clientToken == "" {
|
||||||
err := common.ErrGetClientToken
|
err := common.ErrGetClientToken
|
||||||
logger.Error(ctx, "failed to get client token from context", "error", err)
|
logger.Error(c, "failed to get client token from context", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -32,7 +31,7 @@ func MeasurementLinkHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to unmarshal measurement process request", "error", err)
|
logger.Error(c, "failed to unmarshal measurement process request", "error", err)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -45,9 +44,9 @@ func MeasurementLinkHandler(c *gin.Context) {
|
||||||
pgClient := database.GetPostgresDBClient()
|
pgClient := database.GetPostgresDBClient()
|
||||||
measurementID := request.MeasurementID
|
measurementID := request.MeasurementID
|
||||||
action := request.Action
|
action := request.Action
|
||||||
measurementInfo, err := database.QueryMeasurementByID(ctx, pgClient, measurementID)
|
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, measurementID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "failed to query measurement info by measurement id from postgres", "meauserement_id", measurementID, "error", err)
|
logger.Error(c, "failed to query measurement info by measurement id from postgres", "meauserement_id", measurementID, "error", err)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -60,9 +59,9 @@ func MeasurementLinkHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
componentInfo, err := database.QueryComponentByUUID(ctx, pgClient, measurementInfo.ComponentUUID)
|
componentInfo, err := database.QueryComponentByUUID(c, pgClient, measurementInfo.ComponentUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "failed to query component info by component uuid from postgres", "component_uuid", measurementInfo.ComponentUUID, "error", err)
|
logger.Error(c, "failed to query component info by component uuid from postgres", "component_uuid", measurementInfo.ComponentUUID, "error", err)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
|
|
@ -75,9 +74,9 @@ func MeasurementLinkHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allMeasSet := diagram.NewRedisSet(ctx, constants.RedisAllMeasTagSetKey, 0, false)
|
allMeasSet := diagram.NewRedisSet(c, constants.RedisAllMeasTagSetKey, 0, false)
|
||||||
compMeasLinkKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, componentInfo.Tag)
|
compMeasLinkKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, componentInfo.Tag)
|
||||||
compMeasLinkSet := diagram.NewRedisSet(ctx, compMeasLinkKey, 0, false)
|
compMeasLinkSet := diagram.NewRedisSet(c, compMeasLinkKey, 0, false)
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case constants.SearchLinkAddAction:
|
case constants.SearchLinkAddAction:
|
||||||
|
|
@ -85,18 +84,18 @@ func MeasurementLinkHandler(c *gin.Context) {
|
||||||
err2 := compMeasLinkSet.SADD(measurementInfo.Tag)
|
err2 := compMeasLinkSet.SADD(measurementInfo.Tag)
|
||||||
err = processActionError(err1, err2, action)
|
err = processActionError(err1, err2, action)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "add measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
|
logger.Error(c, "add measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
|
||||||
}
|
}
|
||||||
case constants.SearchLinkDelAction:
|
case constants.SearchLinkDelAction:
|
||||||
err1 := allMeasSet.SREM(measurementInfo.Tag)
|
err1 := allMeasSet.SREM(measurementInfo.Tag)
|
||||||
err2 := compMeasLinkSet.SREM(measurementInfo.Tag)
|
err2 := compMeasLinkSet.SREM(measurementInfo.Tag)
|
||||||
err = processActionError(err1, err2, action)
|
err = processActionError(err1, err2, action)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "del measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
|
logger.Error(c, "del measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
err = common.ErrUnsupportedLinkAction
|
err = common.ErrUnsupportedLinkAction
|
||||||
logger.Error(ctx, "unsupport measurement link process action", "measurement_id", measurementID, "action", action, "error", err)
|
logger.Error(c, "unsupport measurement link process action", "measurement_id", measurementID, "action", action, "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -111,7 +110,7 @@ func MeasurementLinkHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(ctx, "process measurement link success", "measurement_id", measurementID, "action", request.Action)
|
logger.Info(c, "process measurement link success", "measurement_id", measurementID, "action", request.Action)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, network.SuccessResponse{
|
c.JSON(http.StatusOK, network.SuccessResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
|
|
||||||
|
|
@ -35,28 +35,27 @@ var pullUpgrader = websocket.Upgrader{
|
||||||
// @Tags RealTime Component Websocket
|
// @Tags RealTime Component Websocket
|
||||||
// @Router /monitors/data/realtime/stream/:clientID [get]
|
// @Router /monitors/data/realtime/stream/:clientID [get]
|
||||||
func PullRealTimeDataHandler(c *gin.Context) {
|
func PullRealTimeDataHandler(c *gin.Context) {
|
||||||
requestCtx := c.Request.Context()
|
|
||||||
clientID := c.Param("clientID")
|
clientID := c.Param("clientID")
|
||||||
if clientID == "" {
|
if clientID == "" {
|
||||||
err := fmt.Errorf("clientID is missing from the path")
|
err := fmt.Errorf("clientID is missing from the path")
|
||||||
logger.Error(requestCtx, "query clientID from path failed", "error", err, "url", c.Request.RequestURI)
|
logger.Error(c, "query clientID from path failed", "error", err, "url", c.Request.RequestURI)
|
||||||
renderWSRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
renderWSRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := pullUpgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := pullUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(requestCtx, "upgrade http protocol to websocket protocol failed", "error", err)
|
logger.Error(c, "upgrade http protocol to websocket protocol failed", "error", err)
|
||||||
renderWSRespFailure(c, constants.RespCodeServerError, err.Error(), nil)
|
renderWSRespFailure(c, constants.RespCodeServerError, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(requestCtx)
|
ctx, cancel := context.WithCancel(c.Request.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conn.SetCloseHandler(func(code int, text string) error {
|
conn.SetCloseHandler(func(code int, text string) error {
|
||||||
logger.Info(requestCtx, "websocket processor shutdown trigger",
|
logger.Info(c.Request.Context(), "websocket processor shutdown trigger",
|
||||||
"clientID", clientID, "code", code, "reason", text)
|
"clientID", clientID, "code", code, "reason", text)
|
||||||
|
|
||||||
// call cancel to notify other goroutines to stop working
|
// call cancel to notify other goroutines to stop working
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,10 @@ var wsUpgrader = websocket.Upgrader{
|
||||||
//
|
//
|
||||||
// @Router /data/realtime [get]
|
// @Router /data/realtime [get]
|
||||||
func QueryRealTimeDataHandler(c *gin.Context) {
|
func QueryRealTimeDataHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.RealTimeQueryRequest
|
var request network.RealTimeQueryRequest
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to unmarshal real time query request", "error", err)
|
logger.Error(c, "failed to unmarshal real time query request", "error", err)
|
||||||
c.JSON(http.StatusOK, network.FailureResponse{
|
c.JSON(http.StatusOK, network.FailureResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Msg: err.Error(),
|
Msg: err.Error(),
|
||||||
|
|
@ -74,7 +73,7 @@ func QueryRealTimeDataHandler(c *gin.Context) {
|
||||||
|
|
||||||
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "upgrade http protocol to websocket protocal failed", "error", err)
|
logger.Error(c, "upgrade http protocol to websocket protocal failed", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
@ -88,29 +87,29 @@ func QueryRealTimeDataHandler(c *gin.Context) {
|
||||||
case data := <-transportChannel:
|
case data := <-transportChannel:
|
||||||
respByte, err := jsoniter.Marshal(data)
|
respByte, err := jsoniter.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "marshal real time data to bytes failed", "error", err)
|
logger.Error(c, "marshal real time data to bytes failed", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.WriteMessage(websocket.TextMessage, respByte)
|
err = conn.WriteMessage(websocket.TextMessage, respByte)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "write message to websocket connection failed", "error", err)
|
logger.Error(c, "write message to websocket connection failed", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
case <-closeChannel:
|
case <-closeChannel:
|
||||||
logger.Info(ctx, "data receiving goroutine has been closed")
|
logger.Info(c, "data receiving goroutine has been closed")
|
||||||
// TODO 优化时间控制
|
// TODO 优化时间控制
|
||||||
deadline := time.Now().Add(5 * time.Second)
|
deadline := time.Now().Add(5 * time.Second)
|
||||||
err := conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "the session ended normally"), deadline)
|
err := conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "the session ended normally"), deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "sending close control message failed", "error", err)
|
logger.Error(c, "sending close control message failed", "error", err)
|
||||||
}
|
}
|
||||||
// gracefully close session processing
|
// gracefully close session processing
|
||||||
err = conn.Close()
|
err = conn.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "websocket conn closed failed", "error", err)
|
logger.Error(c, "websocket conn closed failed", "error", err)
|
||||||
}
|
}
|
||||||
logger.Info(ctx, "websocket connection closed successfully.")
|
logger.Info(c, "websocket connection closed successfully.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,9 @@ var upgrader = websocket.Upgrader{
|
||||||
|
|
||||||
// RealTimeDataReceivehandler define real time data receive and process API
|
// RealTimeDataReceivehandler define real time data receive and process API
|
||||||
func RealTimeDataReceivehandler(c *gin.Context) {
|
func RealTimeDataReceivehandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "upgrade http protocol to websocket protocal failed", "error", err)
|
logger.Error(c, "upgrade http protocol to websocket protocal failed", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
@ -28,17 +27,17 @@ func RealTimeDataReceivehandler(c *gin.Context) {
|
||||||
for {
|
for {
|
||||||
messageType, p, err := conn.ReadMessage()
|
messageType, p, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "read message from websocket connection failed", "error", err)
|
logger.Error(c, "read message from websocket connection failed", "error", err)
|
||||||
|
|
||||||
respByte := processResponse(-1, "read message from websocket connection failed", nil)
|
respByte := processResponse(-1, "read message from websocket connection failed", nil)
|
||||||
if len(respByte) == 0 {
|
if len(respByte) == 0 {
|
||||||
logger.Error(ctx, "process message from byte failed", "error", err)
|
logger.Error(c, "process message from byte failed", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.WriteMessage(messageType, respByte)
|
err = conn.WriteMessage(messageType, respByte)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "write message to websocket connection failed", "error", err)
|
logger.Error(c, "write message to websocket connection failed", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -47,17 +46,17 @@ func RealTimeDataReceivehandler(c *gin.Context) {
|
||||||
var request network.RealTimeDataReceiveRequest
|
var request network.RealTimeDataReceiveRequest
|
||||||
err = jsoniter.Unmarshal([]byte(p), &request)
|
err = jsoniter.Unmarshal([]byte(p), &request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "unmarshal message from byte failed", "error", err)
|
logger.Error(c, "unmarshal message from byte failed", "error", err)
|
||||||
|
|
||||||
respByte := processResponse(-1, "unmarshal message from byte failed", nil)
|
respByte := processResponse(-1, "unmarshal message from byte failed", nil)
|
||||||
if len(respByte) == 0 {
|
if len(respByte) == 0 {
|
||||||
logger.Error(ctx, "process message from byte failed", "error", err)
|
logger.Error(c, "process message from byte failed", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.WriteMessage(messageType, respByte)
|
err = conn.WriteMessage(messageType, respByte)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "write message to websocket connection failed", "error", err)
|
logger.Error(c, "write message to websocket connection failed", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
@ -71,13 +70,13 @@ func RealTimeDataReceivehandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
respByte := processResponse(0, "success", payload)
|
respByte := processResponse(0, "success", payload)
|
||||||
if len(respByte) == 0 {
|
if len(respByte) == 0 {
|
||||||
logger.Error(ctx, "process message from byte failed", "error", err)
|
logger.Error(c, "process message from byte failed", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.WriteMessage(messageType, respByte)
|
err = conn.WriteMessage(messageType, respByte)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "write message to websocket connection failed", "error", err)
|
logger.Error(c, "write message to websocket connection failed", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,13 +77,12 @@ func init() {
|
||||||
//
|
//
|
||||||
// @Router /monitors/data/subscriptions [post]
|
// @Router /monitors/data/subscriptions [post]
|
||||||
func RealTimeSubHandler(c *gin.Context) {
|
func RealTimeSubHandler(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
|
||||||
var request network.RealTimeSubRequest
|
var request network.RealTimeSubRequest
|
||||||
var subAction string
|
var subAction string
|
||||||
var clientID string
|
var clientID string
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.Error(ctx, "failed to unmarshal real time query request", "error", err)
|
logger.Error(c, "failed to unmarshal real time query request", "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +91,7 @@ func RealTimeSubHandler(c *gin.Context) {
|
||||||
subAction = request.Action
|
subAction = request.Action
|
||||||
id, err := uuid.NewV4()
|
id, err := uuid.NewV4()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "failed to generate client id", "error", err)
|
logger.Error(c, "failed to generate client id", "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -115,9 +114,9 @@ func RealTimeSubHandler(c *gin.Context) {
|
||||||
|
|
||||||
switch subAction {
|
switch subAction {
|
||||||
case constants.SubStartAction:
|
case constants.SubStartAction:
|
||||||
results, err := globalSubState.CreateConfig(ctx, tx, clientID, request.Measurements)
|
results, err := globalSubState.CreateConfig(c, tx, clientID, request.Measurements)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "create real time data subscription config failed", "error", err)
|
logger.Error(c, "create real time data subscription config failed", "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
|
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
TargetResults: results,
|
TargetResults: results,
|
||||||
|
|
@ -131,9 +130,9 @@ func RealTimeSubHandler(c *gin.Context) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
case constants.SubStopAction:
|
case constants.SubStopAction:
|
||||||
results, err := globalSubState.RemoveTargets(ctx, clientID, request.Measurements)
|
results, err := globalSubState.RemoveTargets(c, clientID, request.Measurements)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "remove target to real time data subscription config failed", "error", err)
|
logger.Error(c, "remove target to real time data subscription config failed", "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
|
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
TargetResults: results,
|
TargetResults: results,
|
||||||
|
|
@ -147,9 +146,9 @@ func RealTimeSubHandler(c *gin.Context) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
case constants.SubAppendAction:
|
case constants.SubAppendAction:
|
||||||
results, err := globalSubState.AppendTargets(ctx, tx, clientID, request.Measurements)
|
results, err := globalSubState.AppendTargets(c, tx, clientID, request.Measurements)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "append target to real time data subscription config failed", "error", err)
|
logger.Error(c, "append target to real time data subscription config failed", "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
|
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
TargetResults: results,
|
TargetResults: results,
|
||||||
|
|
@ -163,9 +162,9 @@ func RealTimeSubHandler(c *gin.Context) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
case constants.SubUpdateAction:
|
case constants.SubUpdateAction:
|
||||||
results, err := globalSubState.UpdateTargets(ctx, tx, clientID, request.Measurements)
|
results, err := globalSubState.UpdateTargets(c, tx, clientID, request.Measurements)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "update target to real time data subscription config failed", "error", err)
|
logger.Error(c, "update target to real time data subscription config failed", "error", err)
|
||||||
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
|
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
TargetResults: results,
|
TargetResults: results,
|
||||||
|
|
@ -180,7 +179,7 @@ func RealTimeSubHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
err := fmt.Errorf("%w: request action is %s", common.ErrUnsupportedSubAction, request.Action)
|
err := fmt.Errorf("%w: request action is %s", common.ErrUnsupportedSubAction, request.Action)
|
||||||
logger.Error(ctx, "unsupported action of real time data subscription request", "error", err)
|
logger.Error(c, "unsupported action of real time data subscription request", "error", err)
|
||||||
requestTargetsCount := processRealTimeRequestCount(request.Measurements)
|
requestTargetsCount := processRealTimeRequestCount(request.Measurements)
|
||||||
results := processRealTimeRequestTargets(request.Measurements, requestTargetsCount, constants.CodeUnsupportSubOperation, err)
|
results := processRealTimeRequestTargets(request.Measurements, requestTargetsCount, constants.CodeUnsupportSubOperation, err)
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), network.RealTimeSubPayload{
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), network.RealTimeSubPayload{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package logger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"modelRT/config"
|
||||||
|
"modelRT/constants"
|
||||||
|
"modelRT/logger"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCallerPointsToBusinessCode(t *testing.T) {
|
||||||
|
reader, writer, err := os.Pipe()
|
||||||
|
require.NoError(t, err)
|
||||||
|
originalStdout := os.Stdout
|
||||||
|
os.Stdout = writer
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Stdout = originalStdout
|
||||||
|
_ = reader.Close()
|
||||||
|
_ = writer.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.InitLoggerInstance(config.LoggerConfig{
|
||||||
|
Mode: constants.DevelopmentLogMode,
|
||||||
|
Level: "info",
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.Info(context.Background(), "facade caller test")
|
||||||
|
logger.NewGormLogger().Trace(context.Background(), time.Now(), func() (string, int64) {
|
||||||
|
return "SELECT 1", 1
|
||||||
|
}, nil)
|
||||||
|
require.NoError(t, writer.Close())
|
||||||
|
|
||||||
|
outputBytes, err := io.ReadAll(reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
output := string(outputBytes)
|
||||||
|
|
||||||
|
assert.NotContains(t, output, "logger/facede.go")
|
||||||
|
assert.NotContains(t, output, `"func":"modelRT/logger.Info"`)
|
||||||
|
assert.NotContains(t, output, "gorm.io/gorm")
|
||||||
|
assert.Contains(t, output, "caller_test.go")
|
||||||
|
assert.True(t, strings.Count(output, "modelRT/logger_test.TestCallerPointsToBusinessCode") >= 2)
|
||||||
|
}
|
||||||
|
|
@ -18,8 +18,6 @@ type facade struct {
|
||||||
_logger *zap.Logger
|
_logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
const facadeCallerSkip = 2
|
|
||||||
|
|
||||||
// Debug define facade func of debug level log
|
// Debug define facade func of debug level log
|
||||||
func Debug(ctx context.Context, msg string, kv ...any) {
|
func Debug(ctx context.Context, msg string, kv ...any) {
|
||||||
logFacade().log(ctx, zapcore.DebugLevel, msg, kv...)
|
logFacade().log(ctx, zapcore.DebugLevel, msg, kv...)
|
||||||
|
|
@ -41,16 +39,17 @@ func Error(ctx context.Context, msg string, kv ...any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *facade) log(ctx context.Context, lvl zapcore.Level, msg string, kv ...any) {
|
func (f *facade) log(ctx context.Context, lvl zapcore.Level, msg string, kv ...any) {
|
||||||
f.logSkip(ctx, lvl, 1, msg, kv...)
|
f.logSkip(ctx, lvl, 0, msg, kv...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *facade) logSkip(ctx context.Context, lvl zapcore.Level, extraSkip int, msg string, kv ...any) {
|
func (f *facade) logSkip(ctx context.Context, lvl zapcore.Level, extraSkip int, msg string, kv ...any) {
|
||||||
fields := makeLogFieldsSkip(ctx, extraSkip, kv...)
|
caller := resolveLoggerCaller(extraSkip)
|
||||||
logger := f._logger
|
fields := makeLogFieldsWithCaller(ctx, caller, kv...)
|
||||||
if extraSkip > 0 {
|
ce := f._logger.Check(lvl, msg)
|
||||||
logger = logger.WithOptions(zap.AddCallerSkip(extraSkip))
|
if ce == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ce := logger.Check(lvl, msg)
|
setCheckedEntryCaller(ce, caller)
|
||||||
ce.Write(fields...)
|
ce.Write(fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +71,7 @@ func InfoSkip(ctx context.Context, extraSkip int, msg string, kv ...any) {
|
||||||
func logFacade() *facade {
|
func logFacade() *facade {
|
||||||
fOnce.Do(func() {
|
fOnce.Do(func() {
|
||||||
f = &facade{
|
f = &facade{
|
||||||
_logger: GetLoggerInstance().WithOptions(zap.AddCallerSkip(facadeCallerSkip)),
|
_logger: GetLoggerInstance(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return f
|
return f
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,12 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql
|
||||||
// get gorm exec sql and rows affected
|
// get gorm exec sql and rows affected
|
||||||
sql, rows := fc()
|
sql, rows := fc()
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
ErrorSkip(ctx, 1, "SQL ERROR", "sql", sql, "rows", rows, "dur(ms)", duration)
|
ErrorSkip(ctx, 0, "SQL ERROR", "sql", sql, "rows", rows, "dur(ms)", duration)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if duration > l.SlowThreshold.Milliseconds() {
|
if duration > l.SlowThreshold.Milliseconds() {
|
||||||
WarnSkip(ctx, 1, "SQL SLOW", "sql", sql, "rows", rows, "dur(ms)", duration)
|
WarnSkip(ctx, 0, "SQL SLOW", "sql", sql, "rows", rows, "dur(ms)", duration)
|
||||||
} else {
|
} else {
|
||||||
InfoSkip(ctx, 1, "SQL INFO", "sql", sql, "rows", rows, "dur(ms)", duration)
|
InfoSkip(ctx, 0, "SQL INFO", "sql", sql, "rows", rows, "dur(ms)", duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
@ -41,8 +42,13 @@ func (l *logger) Error(msg string, kv ...any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) log(lvl zapcore.Level, msg string, kv ...any) {
|
func (l *logger) log(lvl zapcore.Level, msg string, kv ...any) {
|
||||||
fields := makeLogFields(l.ctx, kv...)
|
caller := resolveLoggerCaller(0)
|
||||||
|
fields := makeLogFieldsWithCaller(l.ctx, caller, kv...)
|
||||||
ce := l._logger.Check(lvl, msg)
|
ce := l._logger.Check(lvl, msg)
|
||||||
|
if ce == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCheckedEntryCaller(ce, caller)
|
||||||
ce.Write(fields...)
|
ce.Write(fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,6 +57,10 @@ func makeLogFields(ctx context.Context, kv ...any) []zap.Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeLogFieldsSkip(ctx context.Context, extraSkip int, kv ...any) []zap.Field {
|
func makeLogFieldsSkip(ctx context.Context, extraSkip int, kv ...any) []zap.Field {
|
||||||
|
return makeLogFieldsWithCaller(ctx, resolveLoggerCaller(extraSkip), kv...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeLogFieldsWithCaller(ctx context.Context, caller loggerCaller, kv ...any) []zap.Field {
|
||||||
if len(kv)%2 != 0 {
|
if len(kv)%2 != 0 {
|
||||||
kv = append(kv, "unknown")
|
kv = append(kv, "unknown")
|
||||||
}
|
}
|
||||||
|
|
@ -60,8 +70,7 @@ func makeLogFieldsSkip(ctx context.Context, extraSkip int, kv ...any) []zap.Fiel
|
||||||
spanID := spanCtx.SpanID().String()
|
spanID := spanCtx.SpanID().String()
|
||||||
kv = append(kv, "traceID", traceID, "spanID", spanID)
|
kv = append(kv, "traceID", traceID, "spanID", spanID)
|
||||||
|
|
||||||
funcName, file, line := getLoggerCallerInfoSkip(extraSkip)
|
kv = append(kv, "func", caller.funcName, "file", caller.shortFile, "line", caller.line)
|
||||||
kv = append(kv, "func", funcName, "file", file, "line", line)
|
|
||||||
fields := make([]zap.Field, 0, len(kv)/2)
|
fields := make([]zap.Field, 0, len(kv)/2)
|
||||||
for i := 0; i < len(kv); i += 2 {
|
for i := 0; i < len(kv); i += 2 {
|
||||||
key := kv[i].(string)
|
key := kv[i].(string)
|
||||||
|
|
@ -95,13 +104,59 @@ func getLoggerCallerInfo() (funcName, file string, line int) {
|
||||||
|
|
||||||
// getLoggerCallerInfoSkip returns caller info with additional skip frames beyond the standard depth.
|
// getLoggerCallerInfoSkip returns caller info with additional skip frames beyond the standard depth.
|
||||||
func getLoggerCallerInfoSkip(extraSkip int) (funcName, file string, line int) {
|
func getLoggerCallerInfoSkip(extraSkip int) (funcName, file string, line int) {
|
||||||
pc, file, line, ok := runtime.Caller(4 + extraSkip)
|
caller := resolveLoggerCaller(extraSkip)
|
||||||
if !ok {
|
return caller.funcName, caller.shortFile, caller.line
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerCaller struct {
|
||||||
|
pc uintptr
|
||||||
|
funcName string
|
||||||
|
fullFile string
|
||||||
|
shortFile string
|
||||||
|
line int
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveLoggerCaller(extraSkip int) loggerCaller {
|
||||||
|
pcs := make([]uintptr, 32)
|
||||||
|
count := runtime.Callers(2, pcs)
|
||||||
|
frames := runtime.CallersFrames(pcs[:count])
|
||||||
|
|
||||||
|
for {
|
||||||
|
frame, more := frames.Next()
|
||||||
|
if !isLoggerInfrastructureFrame(frame.Function) {
|
||||||
|
if extraSkip > 0 {
|
||||||
|
extraSkip--
|
||||||
|
} else {
|
||||||
|
return loggerCaller{
|
||||||
|
pc: frame.PC,
|
||||||
|
funcName: frame.Function,
|
||||||
|
fullFile: frame.File,
|
||||||
|
shortFile: path.Base(frame.File),
|
||||||
|
line: frame.Line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !more {
|
||||||
|
return loggerCaller{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLoggerInfrastructureFrame(function string) bool {
|
||||||
|
return strings.HasPrefix(function, "modelRT/logger.") ||
|
||||||
|
strings.HasPrefix(function, "gorm.io/gorm")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCheckedEntryCaller(entry *zapcore.CheckedEntry, caller loggerCaller) {
|
||||||
|
if caller.pc == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file = path.Base(file)
|
entry.Entry.Caller = zapcore.EntryCaller{
|
||||||
funcName = runtime.FuncForPC(pc).Name()
|
Defined: true,
|
||||||
return
|
PC: caller.pc,
|
||||||
|
File: caller.fullFile,
|
||||||
|
Line: caller.line,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a logger bound to ctx. Trace fields (traceID, spanID) are extracted
|
// New returns a logger bound to ctx. Trace fields (traceID, spanID) are extracted
|
||||||
|
|
|
||||||
12
main.go
12
main.go
|
|
@ -235,18 +235,6 @@ func main() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentColumnNames, err := database.QueryComponentColumnNames(ctx, tx)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "query component table column names failed", "error", err)
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = model.StoreComponentColumnRecommend(ctx, fullParentPath, isLocalParentPath, componentColumnNames)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "store component column recommend content failed", "error", err)
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allMeasurement, err := database.GetAllMeasurements(ctx, tx)
|
allMeasurement, err := database.GetAllMeasurements(ctx, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "load topologic info from postgres failed", "error", err)
|
logger.Error(ctx, "load topologic info from postgres failed", "error", err)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
|
|
||||||
"github.com/RediSearch/redisearch-go/v2/redisearch"
|
"github.com/RediSearch/redisearch-go/v2/redisearch"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,13 +20,6 @@ type columnParam struct {
|
||||||
AttributeGroup map[string]any
|
AttributeGroup map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type attributeGroupRecommendJob struct {
|
|
||||||
AttributeSet orm.AttributeSet
|
|
||||||
FullPath string
|
|
||||||
IsLocalFullPath string
|
|
||||||
ColumnParam columnParam
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraverseAttributeGroupTables define func to traverse component attribute group tables
|
// TraverseAttributeGroupTables define func to traverse component attribute group tables
|
||||||
func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFullPath map[string]string, isLocalCompTagToFullPath map[string]string, compAttrSet map[string]orm.AttributeSet) error {
|
func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFullPath map[string]string, isLocalCompTagToFullPath map[string]string, compAttrSet map[string]orm.AttributeSet) error {
|
||||||
var tableNames []string
|
var tableNames []string
|
||||||
|
|
@ -46,7 +38,6 @@ func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFul
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs := make([]attributeGroupRecommendJob, 0)
|
|
||||||
for _, tableName := range tableNames {
|
for _, tableName := range tableNames {
|
||||||
var records []map[string]any
|
var records []map[string]any
|
||||||
err := db.Table(tableName).Find(&records).Error
|
err := db.Table(tableName).Find(&records).Error
|
||||||
|
|
@ -111,27 +102,13 @@ func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFul
|
||||||
AttributeType: attributeType,
|
AttributeType: attributeType,
|
||||||
AttributeGroup: attributeGroup,
|
AttributeGroup: attributeGroup,
|
||||||
}
|
}
|
||||||
jobs = append(jobs, attributeGroupRecommendJob{
|
go storeAttributeGroup(ctx, attrSet, fullPath, isLocalfullPath, columnParam)
|
||||||
AttributeSet: attrSet,
|
|
||||||
FullPath: fullPath,
|
|
||||||
IsLocalFullPath: isLocalfullPath,
|
|
||||||
ColumnParam: columnParam,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
group, groupCtx := errgroup.WithContext(ctx)
|
|
||||||
group.SetLimit(16)
|
|
||||||
for _, job := range jobs {
|
|
||||||
job := job
|
|
||||||
group.Go(func() error {
|
|
||||||
return storeAttributeGroup(groupCtx, job.AttributeSet, job.FullPath, job.IsLocalFullPath, job.ColumnParam)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return group.Wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, fullPath string, isLocalFullPath string, colParams columnParam) error {
|
func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, fullPath string, isLocalFullPath string, colParams columnParam) {
|
||||||
rdb := diagram.GetRedisClientInstance()
|
rdb := diagram.GetRedisClientInstance()
|
||||||
pipe := rdb.Pipeline()
|
pipe := rdb.Pipeline()
|
||||||
|
|
||||||
|
|
@ -144,25 +121,18 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
|
||||||
attrbutesGroups := make([]any, 0, len(colParams.AttributeGroup)*2)
|
attrbutesGroups := make([]any, 0, len(colParams.AttributeGroup)*2)
|
||||||
attributeGroupKey := fmt.Sprintf("%s_%s", attributeSet.CompTag, colParams.AttributeType)
|
attributeGroupKey := fmt.Sprintf("%s_%s", attributeSet.CompTag, colParams.AttributeType)
|
||||||
|
|
||||||
sug := make([]redisearch.Suggestion, 0, len(colParams.AttributeGroup)*2+2)
|
sug := make([]redisearch.Suggestion, 0, len(colParams.AttributeGroup)*4)
|
||||||
configTerm := fmt.Sprintf("%s.%s", fullPath, colParams.AttributeType)
|
|
||||||
sug = append(sug, redisearch.Suggestion{
|
|
||||||
Term: configTerm,
|
|
||||||
Score: constants.DefaultScore,
|
|
||||||
})
|
|
||||||
if isLocalFullPath != "" {
|
|
||||||
configTerm = fmt.Sprintf("%s.%s", isLocalFullPath, colParams.AttributeType)
|
|
||||||
sug = append(sug, redisearch.Suggestion{
|
|
||||||
Term: configTerm,
|
|
||||||
Score: constants.DefaultScore,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for attrName, attrValue := range colParams.AttributeGroup {
|
for attrName, attrValue := range colParams.AttributeGroup {
|
||||||
attrbutesGroups = append(attrbutesGroups, attrName, attrValue)
|
attrbutesGroups = append(attrbutesGroups, attrName, attrValue)
|
||||||
attrNameMembers = append(attrNameMembers, attrName)
|
attrNameMembers = append(attrNameMembers, attrName)
|
||||||
|
|
||||||
// add redis fuzzy search suggestion for token1-token7 type
|
// add redis fuzzy search suggestion for token1-token7 type
|
||||||
|
configTerm := fmt.Sprintf("%s.%s", fullPath, colParams.AttributeType)
|
||||||
|
sug = append(sug, redisearch.Suggestion{
|
||||||
|
Term: configTerm,
|
||||||
|
Score: constants.DefaultScore,
|
||||||
|
})
|
||||||
|
|
||||||
measTerm := fmt.Sprintf("%s.%s.%s", fullPath, colParams.AttributeType, attrName)
|
measTerm := fmt.Sprintf("%s.%s.%s", fullPath, colParams.AttributeType, attrName)
|
||||||
sug = append(sug, redisearch.Suggestion{
|
sug = append(sug, redisearch.Suggestion{
|
||||||
Term: measTerm,
|
Term: measTerm,
|
||||||
|
|
@ -170,9 +140,11 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
|
||||||
})
|
})
|
||||||
|
|
||||||
// add redis fuzzy search suggestion for token4-token7 type
|
// add redis fuzzy search suggestion for token4-token7 type
|
||||||
if isLocalFullPath == "" {
|
configTerm = fmt.Sprintf("%s.%s", isLocalFullPath, colParams.AttributeType)
|
||||||
continue
|
sug = append(sug, redisearch.Suggestion{
|
||||||
}
|
Term: configTerm,
|
||||||
|
Score: constants.DefaultScore,
|
||||||
|
})
|
||||||
|
|
||||||
measTerm = fmt.Sprintf("%s.%s.%s", isLocalFullPath, colParams.AttributeType, attrName)
|
measTerm = fmt.Sprintf("%s.%s.%s", isLocalFullPath, colParams.AttributeType, attrName)
|
||||||
sug = append(sug, redisearch.Suggestion{
|
sug = append(sug, redisearch.Suggestion{
|
||||||
|
|
@ -191,24 +163,11 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sug) > 0 {
|
if len(sug) > 0 {
|
||||||
if err := ac.AddTerms(sug...); err != nil {
|
ac.AddTerms(sug...)
|
||||||
logger.Error(ctx, "add attribute group recommend suggestions failed",
|
|
||||||
"component_tag", attributeSet.CompTag,
|
|
||||||
"attribute_type", colParams.AttributeType,
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
return fmt.Errorf("add attribute group recommend suggestions: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := pipe.Exec(ctx)
|
_, err := pipe.Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "init component attribute group recommend content failed",
|
logger.Error(ctx, "init component attribute group recommend content failed", "error", err)
|
||||||
"component_tag", attributeSet.CompTag,
|
|
||||||
"attribute_type", colParams.AttributeType,
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
return fmt.Errorf("init component attribute group recommend content: %w", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
// Package model define model struct of model runtime service
|
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"modelRT/constants"
|
|
||||||
"modelRT/diagram"
|
|
||||||
|
|
||||||
"github.com/RediSearch/redisearch-go/v2/redisearch"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StoreComponentColumnRecommend binds token6 component config to component table column names.
|
|
||||||
func StoreComponentColumnRecommend(ctx context.Context, compTagToFullPath map[string]string, isLocalCompTagToFullPath map[string]string, componentColumnNames []string) error {
|
|
||||||
rdb := diagram.GetRedisClientInstance()
|
|
||||||
pipe := rdb.Pipeline()
|
|
||||||
|
|
||||||
pipe.SAdd(ctx, constants.RedisAllConfigSetKey, constants.ComponentConfigKey)
|
|
||||||
|
|
||||||
if len(componentColumnNames) == 0 {
|
|
||||||
_, err := pipe.Exec(ctx)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
componentColumnMembers := stringSliceToAny(componentColumnNames)
|
|
||||||
pipe.SAdd(ctx, constants.RedisAllMeasTagSetKey, componentColumnMembers...)
|
|
||||||
|
|
||||||
sug := make([]redisearch.Suggestion, 0, len(compTagToFullPath)*len(componentColumnNames)*4)
|
|
||||||
for compTag, fullPath := range compTagToFullPath {
|
|
||||||
specCompMeasKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag)
|
|
||||||
pipe.SAdd(ctx, specCompMeasKey, componentColumnMembers...)
|
|
||||||
pipe.HSet(ctx, componentColumnGroupKey(compTag), componentColumnHashFields(componentColumnNames)...)
|
|
||||||
|
|
||||||
isLocalFullPath := isLocalCompTagToFullPath[compTag]
|
|
||||||
fullConfigTerm := fmt.Sprintf("%s.%s", fullPath, constants.ComponentConfigKey)
|
|
||||||
sug = append(sug, redisearch.Suggestion{
|
|
||||||
Term: fullConfigTerm,
|
|
||||||
Score: constants.DefaultScore,
|
|
||||||
})
|
|
||||||
|
|
||||||
if isLocalFullPath != "" {
|
|
||||||
localConfigTerm := fmt.Sprintf("%s.%s", isLocalFullPath, constants.ComponentConfigKey)
|
|
||||||
sug = append(sug, redisearch.Suggestion{
|
|
||||||
Term: localConfigTerm,
|
|
||||||
Score: constants.DefaultScore,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, columnName := range componentColumnNames {
|
|
||||||
fullColumnTerm := fmt.Sprintf("%s.%s.%s", fullPath, constants.ComponentConfigKey, columnName)
|
|
||||||
sug = append(sug, redisearch.Suggestion{
|
|
||||||
Term: fullColumnTerm,
|
|
||||||
Score: constants.DefaultScore,
|
|
||||||
})
|
|
||||||
|
|
||||||
if isLocalFullPath == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
localColumnTerm := fmt.Sprintf("%s.%s.%s", isLocalFullPath, constants.ComponentConfigKey, columnName)
|
|
||||||
sug = append(sug, redisearch.Suggestion{
|
|
||||||
Term: localColumnTerm,
|
|
||||||
Score: constants.DefaultScore,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(sug) > 0 {
|
|
||||||
ac.AddTerms(sug...)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := pipe.Exec(ctx)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringSliceToAny(values []string) []any {
|
|
||||||
members := make([]any, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
members = append(members, value)
|
|
||||||
}
|
|
||||||
return members
|
|
||||||
}
|
|
||||||
|
|
||||||
func componentColumnGroupKey(compTag string) string {
|
|
||||||
return recommendGroupKey(compTag, constants.ComponentConfigKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func componentColumnHashFields(columnNames []string) []any {
|
|
||||||
return recommendHashFields(columnNames)
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestComponentColumnGroupKey(t *testing.T) {
|
|
||||||
got := componentColumnGroupKey("cable_26-demoProject110kV_TV")
|
|
||||||
want := "cable_26-demoProject110kV_TV_component"
|
|
||||||
if got != want {
|
|
||||||
t.Fatalf("expected key %q, got %q", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestComponentColumnHashFields(t *testing.T) {
|
|
||||||
got := componentColumnHashFields([]string{"global_uuid", "nspath"})
|
|
||||||
want := []any{"global_uuid", true, "nspath", true}
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("expected fields %#v, got %#v", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Package model defines data models and domain rules for model runtime service.
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"modelRT/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
var parameterAttributeGroups = map[string]struct{}{
|
||||||
|
"base_extend": {},
|
||||||
|
"rated": {},
|
||||||
|
"setup": {},
|
||||||
|
"model": {},
|
||||||
|
"stable": {},
|
||||||
|
"craft": {},
|
||||||
|
"integrity": {},
|
||||||
|
"behavior": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClassifyDataObjectToken determines whether token identifies a parameter or a
|
||||||
|
// measurement. Seven-part and four-part tokens are classified by token6, while
|
||||||
|
// two-part tokens are treated as measurements at the current stage.
|
||||||
|
func ClassifyDataObjectToken(token string) (constants.DataObjectType, error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if slices.Contains(parts, "") {
|
||||||
|
return "", fmt.Errorf("invalid data object token %q: token segment cannot be empty", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(parts) {
|
||||||
|
case 2:
|
||||||
|
return constants.DataObjectTypeMeasurement, nil
|
||||||
|
case 4, 7:
|
||||||
|
token6Index := 2
|
||||||
|
if len(parts) == 7 {
|
||||||
|
token6Index = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
token6 := parts[token6Index]
|
||||||
|
if _, ok := parameterAttributeGroups[token6]; ok {
|
||||||
|
return constants.DataObjectTypeParameter, nil
|
||||||
|
}
|
||||||
|
if token6 == "bay" {
|
||||||
|
return constants.DataObjectTypeMeasurement, nil
|
||||||
|
}
|
||||||
|
// TODO 后续要完成对component这个token6类型的支持
|
||||||
|
if token6 == "component" {
|
||||||
|
return "", fmt.Errorf("unsupported data object token %q: token6 component is not queryable", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("invalid data object token %q: unsupported token6 %q", token, token6)
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid data object token %q: expected 2, 4, or 7 segments, got %d", token, len(parts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"modelRT/constants"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClassifyDataObjectToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
token string
|
||||||
|
expected constants.DataObjectType
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "seven-part measurement",
|
||||||
|
token: "grid.zone.station.nspath.component.bay.measurement",
|
||||||
|
expected: constants.DataObjectTypeMeasurement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "four-part measurement",
|
||||||
|
token: "nspath.component.bay.measurement",
|
||||||
|
expected: constants.DataObjectTypeMeasurement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two-part measurement",
|
||||||
|
token: "nspath.measurement",
|
||||||
|
expected: constants.DataObjectTypeMeasurement,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seven-part parameter",
|
||||||
|
token: "grid.zone.station.nspath.component.rated.voltage",
|
||||||
|
expected: constants.DataObjectTypeParameter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "four-part parameter",
|
||||||
|
token: "nspath.component.base_extend.description",
|
||||||
|
expected: constants.DataObjectTypeParameter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "component group",
|
||||||
|
token: "nspath.component.component.name",
|
||||||
|
wantErr: "token6 component is not queryable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown group",
|
||||||
|
token: "nspath.component.unknown.name",
|
||||||
|
wantErr: "unsupported token6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid segment count",
|
||||||
|
token: "grid.zone.station",
|
||||||
|
wantErr: "expected 2, 4, or 7 segments",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty segment",
|
||||||
|
token: "nspath..bay.measurement",
|
||||||
|
wantErr: "token segment cannot be empty",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actual, err := ClassifyDataObjectToken(tt.token)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.wantErr)
|
||||||
|
assert.Empty(t, actual)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassifyDataObjectTokenParameterGroups(t *testing.T) {
|
||||||
|
groups := []string{
|
||||||
|
"base_extend",
|
||||||
|
"rated",
|
||||||
|
"setup",
|
||||||
|
"model",
|
||||||
|
"stable",
|
||||||
|
"craft",
|
||||||
|
"integrity",
|
||||||
|
"behavior",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
t.Run(group, func(t *testing.T) {
|
||||||
|
actual, err := ClassifyDataObjectToken("nspath.component." + group + ".attribute")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, constants.DataObjectTypeParameter, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"modelRT/constants"
|
||||||
|
"modelRT/orm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var allowedMeasurementTypes = map[string]struct{}{
|
||||||
|
"TM": {},
|
||||||
|
"TS": {},
|
||||||
|
"TC": {},
|
||||||
|
"TA": {},
|
||||||
|
"SP": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeasurementTypeFromDataSource returns the two-character measurement type
|
||||||
|
// encoded in a CL3611 channel. Only TM, TS, TC, TA, and SP are valid.
|
||||||
|
func MeasurementTypeFromDataSource(dataSource orm.JSONMap) (string, error) {
|
||||||
|
dataSourceType, err := integerJSONValue(dataSource["type"])
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid measurement data_source type: %w", err)
|
||||||
|
}
|
||||||
|
if dataSourceType != constants.DataSourceTypeCL3611 {
|
||||||
|
return "", fmt.Errorf("measurement type requires data_source type %d, got %d", constants.DataSourceTypeCL3611, dataSourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
ioAddress, ok := dataSource["io_address"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
if value, jsonMapOK := dataSource["io_address"].(orm.JSONMap); jsonMapOK {
|
||||||
|
ioAddress = map[string]any(value)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("measurement data_source io_address is not an object")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, ok := ioAddress["channel"].(string)
|
||||||
|
if !ok || len(channel) < 2 {
|
||||||
|
return "", fmt.Errorf("measurement data_source channel must contain at least two characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
measurementType := strings.ToUpper(channel[:2])
|
||||||
|
if _, ok := allowedMeasurementTypes[measurementType]; !ok {
|
||||||
|
return "", fmt.Errorf("unsupported measurement type %q", measurementType)
|
||||||
|
}
|
||||||
|
return measurementType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func integerJSONValue(value any) (int, error) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case int:
|
||||||
|
return typed, nil
|
||||||
|
case int8:
|
||||||
|
return int(typed), nil
|
||||||
|
case int16:
|
||||||
|
return int(typed), nil
|
||||||
|
case int32:
|
||||||
|
return int(typed), nil
|
||||||
|
case int64:
|
||||||
|
return int(typed), nil
|
||||||
|
case float32:
|
||||||
|
converted := int(typed)
|
||||||
|
if typed != float32(converted) {
|
||||||
|
return 0, fmt.Errorf("expected integer, got %v", typed)
|
||||||
|
}
|
||||||
|
return converted, nil
|
||||||
|
case float64:
|
||||||
|
converted := int(typed)
|
||||||
|
if typed != float64(converted) {
|
||||||
|
return 0, fmt.Errorf("expected integer, got %v", typed)
|
||||||
|
}
|
||||||
|
return converted, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("expected integer, got %T", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"modelRT/orm"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMeasurementTypeFromDataSource(t *testing.T) {
|
||||||
|
for _, measurementType := range []string{"TM", "TS", "TC", "TA", "SP"} {
|
||||||
|
t.Run(measurementType, func(t *testing.T) {
|
||||||
|
actual, err := MeasurementTypeFromDataSource(orm.JSONMap{
|
||||||
|
"type": float64(1),
|
||||||
|
"io_address": map[string]any{
|
||||||
|
"channel": strings.ToLower(measurementType) + "1_test",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, measurementType, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMeasurementTypeFromDataSourceRejectsInvalidValues(t *testing.T) {
|
||||||
|
tests := []orm.JSONMap{
|
||||||
|
{"type": float64(2), "io_address": map[string]any{"channel": "tm1"}},
|
||||||
|
{"type": float64(1), "io_address": map[string]any{"channel": "xx1"}},
|
||||||
|
{"type": float64(1), "io_address": map[string]any{"channel": "t"}},
|
||||||
|
{"type": "1", "io_address": map[string]any{"channel": "tm1"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dataSource := range tests {
|
||||||
|
_, err := MeasurementTypeFromDataSource(dataSource)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,6 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
|
|
||||||
compTagToFullPath := make(map[string]string)
|
compTagToFullPath := make(map[string]string)
|
||||||
isLocalCompTagToFullPath := make(map[string]string)
|
isLocalCompTagToFullPath := make(map[string]string)
|
||||||
var allErrs []error
|
|
||||||
|
|
||||||
zoneToGridPath := make(map[string]string)
|
zoneToGridPath := make(map[string]string)
|
||||||
for gridTag, zoneTags := range measSet.GridToZoneTags {
|
for gridTag, zoneTags := range measSet.GridToZoneTags {
|
||||||
|
|
@ -63,21 +62,12 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
pipe.SAdd(ctx, key, members)
|
pipe.SAdd(ctx, key, members)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
safeAddTerms := func(sug []redisearch.Suggestion) {
|
|
||||||
if len(sug) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := ac.AddTerms(sug...); err != nil {
|
|
||||||
logger.Error(ctx, "add measurement group recommend suggestions failed", "error", err)
|
|
||||||
allErrs = append(allErrs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
safeSAdd(constants.RedisAllGridSetKey, measSet.AllGridTags)
|
safeSAdd(constants.RedisAllGridSetKey, measSet.AllGridTags)
|
||||||
gridSug := util.MapSlice(measSet.AllGridTags, func(gridTag string) redisearch.Suggestion {
|
gridSug := util.MapSlice(measSet.AllGridTags, func(gridTag string) redisearch.Suggestion {
|
||||||
return redisearch.Suggestion{Term: gridTag, Score: constants.DefaultScore}
|
return redisearch.Suggestion{Term: gridTag, Score: constants.DefaultScore}
|
||||||
})
|
})
|
||||||
safeAddTerms(gridSug)
|
ac.AddTerms(gridSug...)
|
||||||
|
|
||||||
safeSAdd(constants.RedisAllZoneSetKey, measSet.AllZoneTags)
|
safeSAdd(constants.RedisAllZoneSetKey, measSet.AllZoneTags)
|
||||||
safeSAdd(constants.RedisAllStationSetKey, measSet.AllStationTags)
|
safeSAdd(constants.RedisAllStationSetKey, measSet.AllStationTags)
|
||||||
|
|
@ -93,7 +83,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
return redisearch.Suggestion{Term: fmt.Sprintf("%s.%s", gridTag, zoneTag), Score: constants.DefaultScore}
|
return redisearch.Suggestion{Term: fmt.Sprintf("%s.%s", gridTag, zoneTag), Score: constants.DefaultScore}
|
||||||
})
|
})
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecGridZoneSetKey, gridTag), zoneTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecGridZoneSetKey, gridTag), zoneTags)
|
||||||
safeAddTerms(sug)
|
ac.AddTerms(sug...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the zone -> stations hierarchy
|
// building the zone -> stations hierarchy
|
||||||
|
|
@ -111,7 +101,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
})
|
})
|
||||||
|
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecZoneStationSetKey, zoneTag), stationTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecZoneStationSetKey, zoneTag), stationTags)
|
||||||
safeAddTerms(sug)
|
ac.AddTerms(sug...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the station -> component nspaths hierarchy
|
// building the station -> component nspaths hierarchy
|
||||||
|
|
@ -132,7 +122,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
sug = append(sug, redisearch.Suggestion{Term: nsPath, Score: constants.DefaultScore})
|
sug = append(sug, redisearch.Suggestion{Term: nsPath, Score: constants.DefaultScore})
|
||||||
}
|
}
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, stationTag), compNSPaths)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, stationTag), compNSPaths)
|
||||||
safeAddTerms(sug)
|
ac.AddTerms(sug...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the component nspath -> component tags hierarchy
|
// building the component nspath -> component tags hierarchy
|
||||||
|
|
@ -146,17 +136,20 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, compTag := range compTags {
|
for _, compTag := range compTags {
|
||||||
fullTerm, localTerm := compTagSuggestionTerms(parentPath, compNSPath, compTag)
|
fullPath := fmt.Sprintf("%s.%s.%s", parentPath, compNSPath, compTag)
|
||||||
compTagToFullPath[compTag] = fullTerm
|
compTagToFullPath[compTag] = fullPath
|
||||||
isLocalCompTagToFullPath[compTag] = localTerm
|
fullPath = fmt.Sprintf("%s.%s", compNSPath, compTag)
|
||||||
|
isLocalCompTagToFullPath[compTag] = fullPath
|
||||||
|
|
||||||
// add redis fuzzy search suggestion for token1-token7 type
|
// add redis fuzzy search suggestion for token1-token7 type
|
||||||
sug = append(sug, redisearch.Suggestion{Term: fullTerm, Score: constants.DefaultScore})
|
term := fullPath
|
||||||
|
sug = append(sug, redisearch.Suggestion{Term: term, Score: constants.DefaultScore})
|
||||||
// add redis fuzzy search suggestion for token4-token7 type
|
// add redis fuzzy search suggestion for token4-token7 type
|
||||||
sug = append(sug, redisearch.Suggestion{Term: localTerm, Score: constants.DefaultScore})
|
term = fmt.Sprintf("%s.%s", compNSPath, compTag)
|
||||||
|
sug = append(sug, redisearch.Suggestion{Term: term, Score: constants.DefaultScore})
|
||||||
}
|
}
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, compNSPath), compTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, compNSPath), compTags)
|
||||||
safeAddTerms(sug)
|
ac.AddTerms(sug...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// building the component tag -> measurement tags hierarchy
|
// building the component tag -> measurement tags hierarchy
|
||||||
|
|
@ -191,25 +184,10 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
sug = append(sug, redisearch.Suggestion{Term: measTerm, Score: constants.DefaultScore})
|
sug = append(sug, redisearch.Suggestion{Term: measTerm, Score: constants.DefaultScore})
|
||||||
}
|
}
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag), measTags)
|
safeSAdd(fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag), measTags)
|
||||||
if len(measTags) > 0 {
|
ac.AddTerms(sug...)
|
||||||
pipe.HSet(ctx, recommendGroupKey(compTag, "bay"), recommendHashFields(measTags)...)
|
|
||||||
}
|
|
||||||
safeAddTerms(sug)
|
|
||||||
}
|
|
||||||
|
|
||||||
// building the component nspath -> measurement tags hierarchy for token4-token7 shorthand
|
|
||||||
for compNSPath, measTags := range measSet.CompNSPathToMeasTags {
|
|
||||||
sug := make([]redisearch.Suggestion, 0, len(measTags))
|
|
||||||
for _, measTag := range measTags {
|
|
||||||
sug = append(sug, redisearch.Suggestion{
|
|
||||||
Term: fmt.Sprintf("%s.%s", compNSPath, measTag),
|
|
||||||
Score: constants.DefaultScore,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, compNSPath), measTags)
|
|
||||||
safeAddTerms(sug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var allErrs []error
|
||||||
cmders, execErr := pipe.Exec(ctx)
|
cmders, execErr := pipe.Exec(ctx)
|
||||||
if execErr != nil {
|
if execErr != nil {
|
||||||
logger.Error(ctx, "pipeline execution failed", "error", execErr)
|
logger.Error(ctx, "pipeline execution failed", "error", execErr)
|
||||||
|
|
@ -233,7 +211,3 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
|
||||||
|
|
||||||
return compTagToFullPath, isLocalCompTagToFullPath, nil
|
return compTagToFullPath, isLocalCompTagToFullPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compTagSuggestionTerms(parentPath string, compNSPath string, compTag string) (string, string) {
|
|
||||||
return fmt.Sprintf("%s.%s.%s", parentPath, compNSPath, compTag), fmt.Sprintf("%s.%s", compNSPath, compTag)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestCompTagSuggestionTermsKeepFullAndLocalPaths(t *testing.T) {
|
|
||||||
parentPath := "grid000.zone000.station000"
|
|
||||||
compNSPath := "110kV_TV-demoProject"
|
|
||||||
compTag := "cable_22-testProject1110kV_TV"
|
|
||||||
|
|
||||||
fullTerm, localTerm := compTagSuggestionTerms(parentPath, compNSPath, compTag)
|
|
||||||
|
|
||||||
wantFullTerm := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV"
|
|
||||||
if fullTerm != wantFullTerm {
|
|
||||||
t.Fatalf("expected full suggestion term %q, got %q", wantFullTerm, fullTerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantLocalTerm := "110kV_TV-demoProject.cable_22-testProject1110kV_TV"
|
|
||||||
if localTerm != wantLocalTerm {
|
|
||||||
t.Fatalf("expected local suggestion term %q, got %q", wantLocalTerm, localTerm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateMeasureIdentifierSupportsJSONNumbers(t *testing.T) {
|
||||||
|
identifier, err := GenerateMeasureIdentifier(map[string]any{
|
||||||
|
"type": float64(2),
|
||||||
|
"io_address": map[string]any{
|
||||||
|
"station": "Station000",
|
||||||
|
"packet": float64(10),
|
||||||
|
"offset": float64(35),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "station000:104:10:35", identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMeasureIdentifierRejectsFractionalJSONNumbers(t *testing.T) {
|
||||||
|
_, err := GenerateMeasureIdentifier(map[string]any{
|
||||||
|
"type": float64(2),
|
||||||
|
"io_address": map[string]any{
|
||||||
|
"station": "station000",
|
||||||
|
"packet": float64(10.5),
|
||||||
|
"offset": float64(35),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
@ -245,24 +245,18 @@ func GenerateMeasureIdentifier(source map[string]any) (string, error) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("Power104: missing packet field")
|
return "", fmt.Errorf("Power104: missing packet field")
|
||||||
}
|
}
|
||||||
var packet int
|
packet, err := integerJSONValue(packetVal)
|
||||||
switch v := packetVal.(type) {
|
if err != nil {
|
||||||
case int:
|
return "", fmt.Errorf("Power104:invalid packet format: %w", err)
|
||||||
packet = v
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("Power104:invalid packet format")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
offsetVal, ok := ioAddress["offset"]
|
offsetVal, ok := ioAddress["offset"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("Power104:missing offset field")
|
return "", fmt.Errorf("Power104:missing offset field")
|
||||||
}
|
}
|
||||||
var offset int
|
offset, err := integerJSONValue(offsetVal)
|
||||||
switch v := offsetVal.(type) {
|
if err != nil {
|
||||||
case int:
|
return "", fmt.Errorf("Power104:invalid offset format: %w", err)
|
||||||
offset = v
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("Power104:invalid offset format")
|
|
||||||
}
|
}
|
||||||
return concatP104WithPlus(station, packet, offset), nil
|
return concatP104WithPlus(station, packet, offset), nil
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// Package model define model struct of model runtime service
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,11 @@ func CleanupRecommendRedisCache(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
patterns := []string{
|
patterns := []string{
|
||||||
"*_zone_tag_keys", // correspond RedisSpecGridZoneSetKey
|
"*_zone_tag_keys", // correspond RedisSpecGridZoneSetKey
|
||||||
"*_station_tag_keys", // correspond RedisSpecZoneStationSetKey
|
"*_station_tag_keys", // correspond RedisSpecZoneStationSetKey
|
||||||
"*_component_nspath_keys", // correspond RedisSpecStationCompNSPATHSetKey
|
"*_component_nspath_keys", // correspond RedisSpecStationCompNSPATHSetKey
|
||||||
"*_component_tag_keys", // correspond RedisSpecCompNSPathCompTagSetKey
|
"*_component_tag_keys", // correspond RedisSpecCompNSPathCompTagSetKey
|
||||||
"*_measurement_tag_keys", // correspond RedisSpecCompTagMeasSetKey
|
"*_measurement_tag_keys", // correspond RedisSpecCompTagMeasSetKey
|
||||||
"*_nspath_measurement_tag_keys", // correspond RedisSpecCompNSPathMeasSetKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pattern := range patterns {
|
for _, pattern := range patterns {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func recommendGroupKey(compTag string, configToken string) string {
|
|
||||||
return fmt.Sprintf("%s_%s", compTag, configToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func recommendHashFields(values []string) []any {
|
|
||||||
fields := make([]any, 0, len(values)*2)
|
|
||||||
for _, value := range values {
|
|
||||||
fields = append(fields, value, true)
|
|
||||||
}
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,945 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"modelRT/constants"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFuzzyRecommendOffsetUsesFullMatchedInput(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
searchPrefix string
|
|
||||||
searchInput string
|
|
||||||
want int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "zone fuzzy prefix",
|
|
||||||
searchPrefix: "grid000",
|
|
||||||
searchInput: "z",
|
|
||||||
want: len([]rune("grid000.z")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "level one fuzzy",
|
|
||||||
searchPrefix: "",
|
|
||||||
searchInput: "g",
|
|
||||||
want: len([]rune("g")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "measurement fuzzy preserves config token",
|
|
||||||
searchPrefix: "grid.zone.station.nspath.comp.config",
|
|
||||||
searchInput: "m",
|
|
||||||
want: len([]rune("grid.zone.station.nspath.comp.config.m")),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := fuzzyRecommendOffset(tt.searchPrefix, tt.searchInput)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Fatalf("expected offset %d, got %d", tt.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFuzzyRecommendMemberSetKeyUsesLocalNSPathSet(t *testing.T) {
|
|
||||||
setKey, ok := fuzzyRecommendMemberSetKey(constants.CompNSPathRecommendHierarchyType, constants.IsLocalRecommendLength, "")
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected local nspath fuzzy member check to use a redis set")
|
|
||||||
}
|
|
||||||
if setKey != constants.RedisAllCompNSPathSetKey {
|
|
||||||
t.Fatalf("expected set key %q, got %q", constants.RedisAllCompNSPathSetKey, setKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMeasurementExactSearchUsesComponentSpecificSet(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
inputSlice []string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "token1 through token7",
|
|
||||||
inputSlice: []string{"grid", "zone", "station", "nspath", "comp_tag", "bay", "measurement"},
|
|
||||||
want: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token4 through token7",
|
|
||||||
inputSlice: []string{"nspath", "comp_tag", "bay", "measurement"},
|
|
||||||
want: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := exactSearchRedisSetKey(constants.MeasTagRecommendHierarchyType, tt.inputSlice, constants.RedisAllMeasTagSetKey)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Fatalf("expected exact search set %q, got %q", tt.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFuzzyExactContinuationValueTreatsMeasurementAsTerminal(t *testing.T) {
|
|
||||||
if got := fuzzyExactContinuationValue(constants.MeasTagRecommendHierarchyType); got != "" {
|
|
||||||
t.Fatalf("expected empty completion for measurement, got %q", got)
|
|
||||||
}
|
|
||||||
if got := fuzzyExactContinuationValue(constants.CompNSPathRecommendHierarchyType); got != "." {
|
|
||||||
t.Fatalf("expected level continuation for component nspath, got %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsKeepsFuzzyExactContinuation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
recommendType constants.RecommendHierarchyType
|
|
||||||
offset int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "local nspath typo completes current level",
|
|
||||||
input: "110kV_TV-demoProjectx",
|
|
||||||
recommendType: constants.CompNSPathRecommendHierarchyType,
|
|
||||||
offset: len([]rune("110kV_TV-demoProject")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "grid typo completes current level",
|
|
||||||
input: "grid000x",
|
|
||||||
recommendType: constants.GridRecommendHierarchyType,
|
|
||||||
offset: len([]rune("grid000")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zone typo completes current level",
|
|
||||||
input: "grid000.zone000x",
|
|
||||||
recommendType: constants.ZoneRecommendHierarchyType,
|
|
||||||
offset: len([]rune("grid000.zone000")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "station typo completes current level",
|
|
||||||
input: "grid000.zone000.station000x",
|
|
||||||
recommendType: constants.StationRecommendHierarchyType,
|
|
||||||
offset: len([]rune("grid000.zone000.station000")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "full nspath typo completes current level",
|
|
||||||
input: "grid000.zone000.station000.110kV_TV-demoProjectx",
|
|
||||||
recommendType: constants.CompNSPathRecommendHierarchyType,
|
|
||||||
offset: len([]rune("grid000.zone000.station000.110kV_TV-demoProject")),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
results := map[string]SearchResult{
|
|
||||||
tt.recommendType.String(): {
|
|
||||||
RecommendType: tt.recommendType,
|
|
||||||
QueryDatas: []string{"."},
|
|
||||||
IsFuzzy: true,
|
|
||||||
Offset: tt.offset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := normalizeRecommendResults(tt.input, results)
|
|
||||||
result := got[tt.recommendType.String()]
|
|
||||||
if result.Offset != tt.offset {
|
|
||||||
t.Fatalf("expected offset %d, got %d", tt.offset, result.Offset)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(result.QueryDatas, []string{"."}) {
|
|
||||||
t.Fatalf("expected fuzzy exact continuation '.', got %#v", result.QueryDatas)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsUsesInputLengthForExactCompletion(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "full token1 to token7 structure",
|
|
||||||
input: "grid000.zone000.station000.220kV_学府路1-testProject1.compTag.config.IA_rms_CTA-testProject1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "local token4 to token7 structure",
|
|
||||||
input: "220kV_学府路1-testProject1.IA_rms_CTA-testProject1",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
results := map[string]SearchResult{
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: []string{""},
|
|
||||||
IsFuzzy: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := normalizeRecommendResults(tt.input, results)
|
|
||||||
result := got[constants.MeasTagRecommendHierarchyType.String()]
|
|
||||||
wantOffset := len([]rune(tt.input))
|
|
||||||
if result.Offset != wantOffset {
|
|
||||||
t.Fatalf("expected offset %d, got %d", wantOffset, result.Offset)
|
|
||||||
}
|
|
||||||
if len(result.QueryDatas) != 0 {
|
|
||||||
t.Fatalf("expected exact completion to return no suffix, got %v", result.QueryDatas)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsUsesInputLengthForLevelContinuation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
recommendType constants.RecommendHierarchyType
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "token1 grid can continue",
|
|
||||||
input: "grid000",
|
|
||||||
recommendType: constants.GridRecommendHierarchyType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 token2 zone can continue",
|
|
||||||
input: "grid000.zone000",
|
|
||||||
recommendType: constants.ZoneRecommendHierarchyType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 token2 token3 station can continue",
|
|
||||||
input: "grid000.zone000.station000",
|
|
||||||
recommendType: constants.StationRecommendHierarchyType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 token2 token3 token4 nspath can continue",
|
|
||||||
input: "grid000.zone000.station000.110kV_TV-demoProject",
|
|
||||||
recommendType: constants.CompNSPathRecommendHierarchyType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 token2 token3 token4 token5 compTag can continue",
|
|
||||||
input: "grid000.zone000.station000.110kV_TV-demoProject.cable_26-demoProject110kV_TV",
|
|
||||||
recommendType: constants.CompTagRecommendHierarchyType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 through token6 config can continue",
|
|
||||||
input: "grid000.zone000.station000.110kV_TV-demoProject.cable_26-demoProject110kV_TV.base_extend",
|
|
||||||
recommendType: constants.ConfigRecommendHierarchyType,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
results := map[string]SearchResult{
|
|
||||||
tt.recommendType.String(): {
|
|
||||||
RecommendType: tt.recommendType,
|
|
||||||
QueryDatas: []string{"."},
|
|
||||||
IsFuzzy: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := normalizeRecommendResults(tt.input, results)
|
|
||||||
result := got[tt.recommendType.String()]
|
|
||||||
wantOffset := len([]rune(tt.input))
|
|
||||||
if result.Offset != wantOffset {
|
|
||||||
t.Fatalf("expected offset %d, got %d", wantOffset, result.Offset)
|
|
||||||
}
|
|
||||||
if len(result.QueryDatas) != 1 || result.QueryDatas[0] != "." {
|
|
||||||
t.Fatalf("expected level continuation '.', got %v", result.QueryDatas)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsKeepsFallbackOffsetZero(t *testing.T) {
|
|
||||||
input := "x"
|
|
||||||
results := map[string]SearchResult{
|
|
||||||
constants.GridRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.GridRecommendHierarchyType,
|
|
||||||
QueryDatas: []string{"grid000", "grid001"},
|
|
||||||
IsFuzzy: true,
|
|
||||||
IsFallback: true,
|
|
||||||
Offset: 0,
|
|
||||||
},
|
|
||||||
constants.CompNSPathRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.CompNSPathRecommendHierarchyType,
|
|
||||||
QueryDatas: []string{"nspath000", "nspath001"},
|
|
||||||
IsFuzzy: true,
|
|
||||||
IsFallback: true,
|
|
||||||
Offset: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := normalizeRecommendResults(input, results)
|
|
||||||
for key, result := range got {
|
|
||||||
if result.Offset != 0 {
|
|
||||||
t.Fatalf("expected fallback offset 0 for %s, got %d", key, result.Offset)
|
|
||||||
}
|
|
||||||
if len(result.QueryDatas) != 2 {
|
|
||||||
t.Fatalf("expected fallback recommends to remain intact for %s, got %v", key, result.QueryDatas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsKeepsParentPrefixOffsetForSpecificFallback(t *testing.T) {
|
|
||||||
nsPath := "220kV_学府路1-testProject1"
|
|
||||||
input := nsPath + ".x"
|
|
||||||
offset := recommendPrefixOffset([]string{nsPath})
|
|
||||||
results := map[string]SearchResult{
|
|
||||||
constants.CompTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.CompTagRecommendHierarchyType,
|
|
||||||
QueryDatas: []string{
|
|
||||||
nsPath + ".CTA-testProject1220kV_学府路1",
|
|
||||||
nsPath + ".CB-testProject1220kV_学府路1",
|
|
||||||
},
|
|
||||||
IsFuzzy: true,
|
|
||||||
IsFallback: true,
|
|
||||||
Offset: offset,
|
|
||||||
},
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: []string{
|
|
||||||
nsPath + ".IA_rms_CTA-testProject1",
|
|
||||||
nsPath + ".IB_rms_CTA-testProject1",
|
|
||||||
},
|
|
||||||
IsFuzzy: true,
|
|
||||||
IsFallback: true,
|
|
||||||
Offset: offset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := normalizeRecommendResults(input, results)
|
|
||||||
if offset != 24 {
|
|
||||||
t.Fatalf("expected sample parent prefix offset 24, got %d", offset)
|
|
||||||
}
|
|
||||||
for key, result := range got {
|
|
||||||
if result.Offset != 24 {
|
|
||||||
t.Fatalf("expected specific fallback offset 24 for %s, got %d", key, result.Offset)
|
|
||||||
}
|
|
||||||
for _, recommend := range result.QueryDatas {
|
|
||||||
if strings.HasPrefix(recommend, nsPath+".") {
|
|
||||||
t.Fatalf("expected trimmed fallback recommend for %s, got %s", key, recommend)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsTrimsConfigFuzzySuffixes(t *testing.T) {
|
|
||||||
input := "110kV_TV-testProject1.cable_22-testProject1110kV_TV.base_extend.c"
|
|
||||||
offset := len([]rune(input))
|
|
||||||
results := map[string]SearchResult{
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: []string{
|
|
||||||
input + "apacity",
|
|
||||||
input + "ategory",
|
|
||||||
input + "urrent",
|
|
||||||
input + "ode",
|
|
||||||
},
|
|
||||||
IsFuzzy: true,
|
|
||||||
Offset: offset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := normalizeRecommendResults(input, results)
|
|
||||||
result := got[constants.MeasTagRecommendHierarchyType.String()]
|
|
||||||
want := []string{"apacity", "ategory", "urrent", "ode"}
|
|
||||||
if result.Offset != offset {
|
|
||||||
t.Fatalf("expected offset %d, got %d", offset, result.Offset)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(result.QueryDatas, want) {
|
|
||||||
t.Fatalf("expected trimmed recommends %#v, got %#v", want, result.QueryDatas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsTrimsToken4FallbackCandidates(t *testing.T) {
|
|
||||||
nsPath := "110kV_TV-testProject1"
|
|
||||||
input := nsPath + ".x"
|
|
||||||
offset := recommendPrefixOffset([]string{nsPath})
|
|
||||||
results := map[string]SearchResult{
|
|
||||||
constants.CompTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.CompTagRecommendHierarchyType,
|
|
||||||
QueryDatas: []string{
|
|
||||||
nsPath + ".cable_22-testProject1110kV_TV",
|
|
||||||
nsPath + ".cable_23-testProject1110kV_TV",
|
|
||||||
},
|
|
||||||
IsFuzzy: true,
|
|
||||||
IsFallback: true,
|
|
||||||
Offset: offset,
|
|
||||||
},
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: []string{
|
|
||||||
nsPath + ".IA_rms_CTA-testProject1",
|
|
||||||
nsPath + ".IB_rms_CTA-testProject1",
|
|
||||||
},
|
|
||||||
IsFuzzy: true,
|
|
||||||
IsFallback: true,
|
|
||||||
Offset: offset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got := normalizeRecommendResults(input, results)
|
|
||||||
for key, result := range got {
|
|
||||||
if result.Offset != offset {
|
|
||||||
t.Fatalf("expected offset %d for %s, got %d", offset, key, result.Offset)
|
|
||||||
}
|
|
||||||
for _, recommend := range result.QueryDatas {
|
|
||||||
if strings.HasPrefix(recommend, nsPath+".") {
|
|
||||||
t.Fatalf("expected trimmed token4 fallback recommend for %s, got %s", key, recommend)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if got[constants.CompTagRecommendHierarchyType.String()].QueryDatas[0] != "cable_22-testProject1110kV_TV" {
|
|
||||||
t.Fatalf("expected token5 suffixes, got %v", got[constants.CompTagRecommendHierarchyType.String()].QueryDatas)
|
|
||||||
}
|
|
||||||
if got[constants.MeasTagRecommendHierarchyType.String()].QueryDatas[0] != "IA_rms_CTA-testProject1" {
|
|
||||||
t.Fatalf("expected token7 suffixes, got %v", got[constants.MeasTagRecommendHierarchyType.String()].QueryDatas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsKeepsFullAndLocalConfigMeasurementSuffixesConsistent(t *testing.T) {
|
|
||||||
fullPrefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.base_extend"
|
|
||||||
localPrefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV.base_extend"
|
|
||||||
members := []string{"capacity", "category", "current"}
|
|
||||||
|
|
||||||
fullResults := map[string]SearchResult{
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(fullPrefix+".", "."), members),
|
|
||||||
IsFuzzy: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
localResults := map[string]SearchResult{
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(localPrefix+".", "."), members),
|
|
||||||
IsFuzzy: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fullGot := normalizeRecommendResults(fullPrefix+".", fullResults)
|
|
||||||
localGot := normalizeRecommendResults(localPrefix+".", localResults)
|
|
||||||
fullRecommends := fullGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
|
|
||||||
localRecommends := localGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
|
|
||||||
if !reflect.DeepEqual(fullRecommends, localRecommends) {
|
|
||||||
t.Fatalf("expected full/local config measurement suffixes to match, full=%#v local=%#v", fullRecommends, localRecommends)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsKeepsFullAndLocalBayMeasurementSuffixesConsistent(t *testing.T) {
|
|
||||||
fullPrefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
|
|
||||||
localPrefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
|
|
||||||
members := []string{"IA_rms", "IB_rms", "IC_rms"}
|
|
||||||
|
|
||||||
fullResults := map[string]SearchResult{
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(fullPrefix+".", "."), members),
|
|
||||||
IsFuzzy: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
localResults := map[string]SearchResult{
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(localPrefix+".", "."), members),
|
|
||||||
IsFuzzy: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fullGot := normalizeRecommendResults(fullPrefix+".", fullResults)
|
|
||||||
localGot := normalizeRecommendResults(localPrefix+".", localResults)
|
|
||||||
fullRecommends := fullGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
|
|
||||||
localRecommends := localGot[constants.MeasTagRecommendHierarchyType.String()].QueryDatas
|
|
||||||
if !reflect.DeepEqual(fullRecommends, localRecommends) {
|
|
||||||
t.Fatalf("expected full/local bay measurement suffixes to match, full=%#v local=%#v", fullRecommends, localRecommends)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsKeepsConfigMismatchFallbackConsistentWithEmptyInput(t *testing.T) {
|
|
||||||
prefix := "110kV_TV-demoProject.cable_22-testProject1110kV_TV"
|
|
||||||
members := []string{"bay", "base_extend", "model", "component"}
|
|
||||||
groupResults := combineQueryResultByInput(constants.ConfigRecommendHierarchyType, constants.IsLocalRecommendLength, strings.Split(prefix+".", "."), members)
|
|
||||||
offset := recommendPrefixOffset(strings.Split(prefix, "."))
|
|
||||||
|
|
||||||
emptyInputResults := map[string]SearchResult{
|
|
||||||
constants.ConfigRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.ConfigRecommendHierarchyType,
|
|
||||||
QueryDatas: append([]string{}, groupResults...),
|
|
||||||
IsFuzzy: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mismatchFallbackResults := map[string]SearchResult{
|
|
||||||
constants.ConfigRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.ConfigRecommendHierarchyType,
|
|
||||||
QueryDatas: append([]string{}, groupResults...),
|
|
||||||
IsFuzzy: true,
|
|
||||||
IsFallback: true,
|
|
||||||
Offset: offset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
emptyGot := normalizeRecommendResults(prefix+".", emptyInputResults)
|
|
||||||
mismatchGot := normalizeRecommendResults(prefix+".x", mismatchFallbackResults)
|
|
||||||
emptyResult := emptyGot[constants.ConfigRecommendHierarchyType.String()]
|
|
||||||
mismatchResult := mismatchGot[constants.ConfigRecommendHierarchyType.String()]
|
|
||||||
|
|
||||||
if mismatchResult.Offset != emptyResult.Offset {
|
|
||||||
t.Fatalf("expected config mismatch fallback offset %d, got %d", emptyResult.Offset, mismatchResult.Offset)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(mismatchResult.QueryDatas, emptyResult.QueryDatas) {
|
|
||||||
t.Fatalf("expected config mismatch fallback recommends to match empty input, empty=%#v mismatch=%#v", emptyResult.QueryDatas, mismatchResult.QueryDatas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNormalizeRecommendResultsKeepsMeasurementMismatchFallbackConsistentWithEmptyInput(t *testing.T) {
|
|
||||||
prefix := "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay"
|
|
||||||
members := []string{"IA_rms_CTA-testProject1", "IB_rms_CTA-testProject1", "IC_rms_CTA-testProject1"}
|
|
||||||
groupResults := combineQueryResultByInput(constants.MeasTagRecommendHierarchyType, constants.FullRecommendLength, strings.Split(prefix+".", "."), members)
|
|
||||||
offset := recommendPrefixOffset(strings.Split(prefix, "."))
|
|
||||||
|
|
||||||
emptyInputResults := map[string]SearchResult{
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: append([]string{}, groupResults...),
|
|
||||||
IsFuzzy: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mismatchFallbackResults := map[string]SearchResult{
|
|
||||||
constants.MeasTagRecommendHierarchyType.String(): {
|
|
||||||
RecommendType: constants.MeasTagRecommendHierarchyType,
|
|
||||||
QueryDatas: append([]string{}, groupResults...),
|
|
||||||
IsFuzzy: true,
|
|
||||||
IsFallback: true,
|
|
||||||
Offset: offset,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
emptyGot := normalizeRecommendResults(prefix+".", emptyInputResults)
|
|
||||||
mismatchGot := normalizeRecommendResults(prefix+".x", mismatchFallbackResults)
|
|
||||||
emptyResult := emptyGot[constants.MeasTagRecommendHierarchyType.String()]
|
|
||||||
mismatchResult := mismatchGot[constants.MeasTagRecommendHierarchyType.String()]
|
|
||||||
|
|
||||||
if mismatchResult.Offset != emptyResult.Offset {
|
|
||||||
t.Fatalf("expected measurement mismatch fallback offset %d, got %d", emptyResult.Offset, mismatchResult.Offset)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(mismatchResult.QueryDatas, emptyResult.QueryDatas) {
|
|
||||||
t.Fatalf("expected measurement mismatch fallback recommends to match empty input, empty=%#v mismatch=%#v", emptyResult.QueryDatas, mismatchResult.QueryDatas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRecommendGroupTokens(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
inputSlice []string
|
|
||||||
wantCompTag string
|
|
||||||
wantConfig string
|
|
||||||
wantOK bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "full token1 to token7 input",
|
|
||||||
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "base_extend", ""},
|
|
||||||
wantCompTag: "comp_tag",
|
|
||||||
wantConfig: "base_extend",
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "local token4 to token7 input",
|
|
||||||
inputSlice: []string{"nspath", "comp_tag", "component", ""},
|
|
||||||
wantCompTag: "comp_tag",
|
|
||||||
wantConfig: "component",
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing config token",
|
|
||||||
inputSlice: []string{"nspath", ""},
|
|
||||||
wantOK: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gotCompTag, gotConfig, gotOK := recommendGroupTokens(tt.inputSlice)
|
|
||||||
if gotOK != tt.wantOK {
|
|
||||||
t.Fatalf("expected ok %t, got %t", tt.wantOK, gotOK)
|
|
||||||
}
|
|
||||||
if gotCompTag != tt.wantCompTag || gotConfig != tt.wantConfig {
|
|
||||||
t.Fatalf("expected compTag/config %q/%q, got %q/%q", tt.wantCompTag, tt.wantConfig, gotCompTag, gotConfig)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigRecommendCompTag(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
inputSlice []string
|
|
||||||
want string
|
|
||||||
wantOK bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "full token1 to token6 input",
|
|
||||||
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "b"},
|
|
||||||
want: "comp_tag",
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "local token4 to token6 input",
|
|
||||||
inputSlice: []string{"nspath", "comp_tag", "b"},
|
|
||||||
want: "comp_tag",
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing comp tag",
|
|
||||||
inputSlice: []string{"nspath"},
|
|
||||||
wantOK: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, gotOK := configRecommendCompTag(tt.inputSlice)
|
|
||||||
if gotOK != tt.wantOK {
|
|
||||||
t.Fatalf("expected ok %t, got %t", tt.wantOK, gotOK)
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Fatalf("expected compTag %q, got %q", tt.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterMembersByTrimmedPrefix(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
members []string
|
|
||||||
searchInput string
|
|
||||||
wantMembers []string
|
|
||||||
wantMatchInput string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "config typo falls back to previous rune",
|
|
||||||
members: []string{"bay", "base_extend", "model", "rated", "stable", "component"},
|
|
||||||
searchInput: "bx",
|
|
||||||
wantMembers: []string{"bay", "base_extend"},
|
|
||||||
wantMatchInput: "b",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "measurement typo falls back to previous rune",
|
|
||||||
members: []string{"I_A_rms", "I_B_rms", "U_A_rms"},
|
|
||||||
searchInput: "Ix",
|
|
||||||
wantMembers: []string{"I_A_rms", "I_B_rms"},
|
|
||||||
wantMatchInput: "I",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no fallback to empty prefix",
|
|
||||||
members: []string{"bay", "base_extend"},
|
|
||||||
searchInput: "x",
|
|
||||||
wantMembers: []string{},
|
|
||||||
wantMatchInput: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gotMembers, gotMatchInput := filterMembersByTrimmedPrefix(tt.members, tt.searchInput)
|
|
||||||
if !reflect.DeepEqual(gotMembers, tt.wantMembers) {
|
|
||||||
t.Fatalf("expected members %#v, got %#v", tt.wantMembers, gotMembers)
|
|
||||||
}
|
|
||||||
if gotMatchInput != tt.wantMatchInput {
|
|
||||||
t.Fatalf("expected matched input %q, got %q", tt.wantMatchInput, gotMatchInput)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDotAndTypoInputsShareTrimmedPrefixFallback(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
dotInput string
|
|
||||||
typoInput string
|
|
||||||
members []string
|
|
||||||
wantSuffix []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "token1 grid",
|
|
||||||
dotInput: "g.",
|
|
||||||
typoInput: "gx",
|
|
||||||
members: []string{"grid000", "grid001", "zone000"},
|
|
||||||
wantSuffix: []string{"grid000", "grid001"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token2 zone",
|
|
||||||
dotInput: "z.",
|
|
||||||
typoInput: "zx",
|
|
||||||
members: []string{"zone000", "zone001", "station000"},
|
|
||||||
wantSuffix: []string{"zone000", "zone001"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token3 station",
|
|
||||||
dotInput: "s.",
|
|
||||||
typoInput: "sx",
|
|
||||||
members: []string{"station000", "station001", "zone000"},
|
|
||||||
wantSuffix: []string{"station000", "station001"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token4 nspath",
|
|
||||||
dotInput: "1.",
|
|
||||||
typoInput: "1x",
|
|
||||||
members: []string{"110kV_TV-demoProject", "110kV_TV-testProject1", "220kV_TV-demoProject"},
|
|
||||||
wantSuffix: []string{"110kV_TV-demoProject", "110kV_TV-testProject1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token5 component tag",
|
|
||||||
dotInput: "c.",
|
|
||||||
typoInput: "cx",
|
|
||||||
members: []string{"cable_22-testProject1110kV_TV", "cable_26-demoProject110kV_TV", "bay"},
|
|
||||||
wantSuffix: []string{"cable_22-testProject1110kV_TV", "cable_26-demoProject110kV_TV"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token6 config",
|
|
||||||
dotInput: "b.",
|
|
||||||
typoInput: "bx",
|
|
||||||
members: []string{"bay", "base_extend", "component"},
|
|
||||||
wantSuffix: []string{"bay", "base_extend"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token7 measurement",
|
|
||||||
dotInput: "I.",
|
|
||||||
typoInput: "Ix",
|
|
||||||
members: []string{"IA_rms", "IB_rms", "UA_rms"},
|
|
||||||
wantSuffix: []string{"IA_rms", "IB_rms"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
dotSearchInput := strings.TrimSuffix(tt.dotInput, ".")
|
|
||||||
typoSearchInput := trimLastRune(tt.typoInput)
|
|
||||||
dotMembers, dotMatchedInput := filterMembersByTrimmedPrefix(tt.members, dotSearchInput)
|
|
||||||
typoMembers, typoMatchedInput := filterMembersByTrimmedPrefix(tt.members, typoSearchInput)
|
|
||||||
|
|
||||||
if dotMatchedInput != typoMatchedInput {
|
|
||||||
t.Fatalf("expected dot and typo matched input to be equal, dot=%q typo=%q", dotMatchedInput, typoMatchedInput)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(dotMembers, typoMembers) {
|
|
||||||
t.Fatalf("expected dot and typo members to be equal, dot=%#v typo=%#v", dotMembers, typoMembers)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(dotMembers, tt.wantSuffix) {
|
|
||||||
t.Fatalf("expected members %#v, got %#v", tt.wantSuffix, dotMembers)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExactMatchChecksForInput(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
inputSlice []string
|
|
||||||
want []exactMatchCheck
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "token1 can be grid or local nspath",
|
|
||||||
inputSlice: []string{"g"},
|
|
||||||
want: []exactMatchCheck{
|
|
||||||
{setKey: constants.RedisAllGridSetKey, member: "g"},
|
|
||||||
{setKey: constants.RedisAllCompNSPathSetKey, member: "g"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 token2 can be zone comp tag or nspath meas",
|
|
||||||
inputSlice: []string{"grid000", "z"},
|
|
||||||
want: []exactMatchCheck{
|
|
||||||
{setKey: fmt.Sprintf(constants.RedisSpecGridZoneSetKey, "grid000"), member: "z"},
|
|
||||||
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "grid000"), member: "z"},
|
|
||||||
{setKey: constants.RedisAllCompTagSetKey, member: "z"},
|
|
||||||
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, "grid000"), member: "z"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 token2 token3 can be station or local config",
|
|
||||||
inputSlice: []string{"grid000", "zone000", "s"},
|
|
||||||
want: []exactMatchCheck{
|
|
||||||
{setKey: fmt.Sprintf(constants.RedisSpecZoneStationSetKey, "zone000"), member: "s"},
|
|
||||||
{setKey: constants.RedisAllConfigSetKey, member: "s"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 token2 token3 token4 can be nspath or local meas",
|
|
||||||
inputSlice: []string{"grid000", "zone000", "station000", "1"},
|
|
||||||
want: []exactMatchCheck{
|
|
||||||
{setKey: fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, "station000"), member: "1"},
|
|
||||||
{setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "zone000"), member: "1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 through token5 component tag",
|
|
||||||
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "c"},
|
|
||||||
want: []exactMatchCheck{
|
|
||||||
{setKey: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "nspath"), member: "c"},
|
|
||||||
{setKey: constants.RedisAllCompTagSetKey, member: "c"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 through token6 config",
|
|
||||||
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "b"},
|
|
||||||
want: []exactMatchCheck{
|
|
||||||
{setKey: constants.RedisAllConfigSetKey, member: "b"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token1 through token7 measurement",
|
|
||||||
inputSlice: []string{"grid000", "zone000", "station000", "nspath", "comp_tag", "bay", "I"},
|
|
||||||
want: []exactMatchCheck{
|
|
||||||
{setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"), member: "I"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := exactMatchChecksForInput(tt.inputSlice)
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Fatalf("expected checks %#v, got %#v", tt.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteMeasurementTrailingDotChecks(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
wantOffset int
|
|
||||||
wantCheck exactMatchCheck
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "token1 through token7",
|
|
||||||
input: "grid000.zone000.station000.110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay.IA_rms_CTA-testProject1.",
|
|
||||||
wantOffset: 105,
|
|
||||||
wantCheck: exactMatchCheck{
|
|
||||||
setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "cable_22-testProject1110kV_TV"),
|
|
||||||
member: "IA_rms_CTA-testProject1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token4 through token7",
|
|
||||||
input: "110kV_TV-demoProject.cable_22-testProject1110kV_TV.bay.IA_rms_CTA-testProject1.",
|
|
||||||
wantOffset: 78,
|
|
||||||
wantCheck: exactMatchCheck{
|
|
||||||
setKey: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "cable_22-testProject1110kV_TV"),
|
|
||||||
member: "IA_rms_CTA-testProject1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "token4 and token7",
|
|
||||||
input: "110kV_TV-testProject1.IA_rms_CTA-testProject1.",
|
|
||||||
wantOffset: 45,
|
|
||||||
wantCheck: exactMatchCheck{
|
|
||||||
setKey: fmt.Sprintf(constants.RedisSpecCompNSPathMeasSetKey, "110kV_TV-testProject1"),
|
|
||||||
member: "IA_rms_CTA-testProject1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
measurement := strings.TrimSuffix(tt.input, ".")
|
|
||||||
if got := len([]rune(measurement)); got != tt.wantOffset {
|
|
||||||
t.Fatalf("expected offset %d, got %d", tt.wantOffset, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, ok := completeMeasurementExactMatchCheck(strings.Split(measurement, "."))
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected complete measurement exact-match check")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.wantCheck) {
|
|
||||||
t.Fatalf("expected check %#v, got %#v", tt.wantCheck, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFallbackSpecificSetKey(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
hierarchy constants.RecommendHierarchyType
|
|
||||||
inputSlice []string
|
|
||||||
want string
|
|
||||||
wantOK bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "zone fallback uses grid specific set",
|
|
||||||
hierarchy: constants.ZoneRecommendHierarchyType,
|
|
||||||
inputSlice: []string{"grid000", "z"},
|
|
||||||
want: fmt.Sprintf(constants.RedisSpecGridZoneSetKey, "grid000"),
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component tag fallback uses previous nspath token",
|
|
||||||
hierarchy: constants.CompTagRecommendHierarchyType,
|
|
||||||
inputSlice: []string{"grid000", "I"},
|
|
||||||
want: fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, "grid000"),
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "measurement fallback skips config token and uses component tag",
|
|
||||||
hierarchy: constants.MeasTagRecommendHierarchyType,
|
|
||||||
inputSlice: []string{"nspath", "comp_tag", "config", "m"},
|
|
||||||
want: fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, "comp_tag"),
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "config fallback has no parent specific set",
|
|
||||||
hierarchy: constants.ConfigRecommendHierarchyType,
|
|
||||||
inputSlice: []string{"nspath", "comp_tag", ""},
|
|
||||||
wantOK: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing previous token",
|
|
||||||
hierarchy: constants.ZoneRecommendHierarchyType,
|
|
||||||
inputSlice: []string{"z"},
|
|
||||||
wantOK: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, ok := fallbackSpecificSetKey(tt.hierarchy, tt.inputSlice)
|
|
||||||
if ok != tt.wantOK {
|
|
||||||
t.Fatalf("expected ok %t, got %t", tt.wantOK, ok)
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Fatalf("expected key %q, got %q", tt.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldFallbackToInitialRecommend(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{input: "", want: false},
|
|
||||||
{input: ".", want: true},
|
|
||||||
{input: ".x", want: true},
|
|
||||||
{input: "..x", want: true},
|
|
||||||
{input: "...x", want: true},
|
|
||||||
{input: "grid000", want: false},
|
|
||||||
{input: "grid000.", want: false},
|
|
||||||
{input: "grid000.zone000", want: false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
|
||||||
got := shouldFallbackToInitialRecommend(tt.input)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Fatalf("expected %t, got %t", tt.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -26,6 +26,7 @@ type WSResponse struct {
|
||||||
type MeasurementRecommendPayload struct {
|
type MeasurementRecommendPayload struct {
|
||||||
Input string `json:"input" example:"transformfeeder1_220."`
|
Input string `json:"input" example:"transformfeeder1_220."`
|
||||||
Offset int `json:"offset" example:"21"`
|
Offset int `json:"offset" example:"21"`
|
||||||
|
RecommendType string `json:"recommended_type" example:"grid_tag"`
|
||||||
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"`
|
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ func (a *AsyncTask) IsFailed() bool {
|
||||||
return a.Status == AsyncTaskStatusFailed
|
return a.Status == AsyncTaskStatusFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidTaskType checks if the task type is valid
|
// IsValidAsyncTaskType checks if the task type is valid
|
||||||
func IsValidAsyncTaskType(taskType string) bool {
|
func IsValidAsyncTaskType(taskType string) bool {
|
||||||
switch AsyncTaskType(taskType) {
|
switch AsyncTaskType(taskType) {
|
||||||
case AsyncTaskTypeTopologyAnalysis, AsyncTaskTypePerformanceAnalysis,
|
case AsyncTaskTypeTopologyAnalysis, AsyncTaskTypePerformanceAnalysis,
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,7 @@ func NewBusbarSection(name string) (*BusbarSection, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BusbarSection) BusNameLenCheck() bool {
|
func (b *BusbarSection) BusNameLenCheck() bool {
|
||||||
if len([]rune(b.BusbarName)) > 20 {
|
return len([]rune(b.BusbarName)) <= 20
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BusbarSection) BusVoltageCheck() bool {
|
func (b *BusbarSection) BusVoltageCheck() bool {
|
||||||
|
|
@ -105,8 +102,5 @@ func (b *BusbarSection) BusVoltageCheck() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BusbarSection) BusDescLenCheck() bool {
|
func (b *BusbarSection) BusDescLenCheck() bool {
|
||||||
if len([]rune(b.BusbarDesc)) > 100 {
|
return len([]rune(b.BusbarDesc)) <= 100
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ type Measurement struct {
|
||||||
Name string `gorm:"column:name;size:64;not null;default:''"`
|
Name string `gorm:"column:name;size:64;not null;default:''"`
|
||||||
Type int16 `gorm:"column:type;not null;default:-1"`
|
Type int16 `gorm:"column:type;not null;default:-1"`
|
||||||
Size int `gorm:"column:size;not null;default:-1"`
|
Size int `gorm:"column:size;not null;default:-1"`
|
||||||
|
Mode int16 `gorm:"column:mode;not null;default:1"`
|
||||||
DataSource JSONMap `gorm:"column:data_source;type:jsonb;not null;default:'{}'"`
|
DataSource JSONMap `gorm:"column:data_source;type:jsonb;not null;default:'{}'"`
|
||||||
EventPlan JSONMap `gorm:"column:event_plan;type:jsonb;not null;default:'{}'"`
|
EventPlan JSONMap `gorm:"column:event_plan;type:jsonb;not null;default:'{}'"`
|
||||||
Binding JSONMap `gorm:"column:binding;type:jsonb;not null;default:'{\"ct\":{\"ratio\":1.0,\"polarity\":1,\"index\":0},\"pt\":{\"ratio\":1.0,\"polarity\":1,\"index\":0}}'"`
|
Binding JSONMap `gorm:"column:binding;type:jsonb;not null;default:'{\"ct\":{\"ratio\":1.0,\"polarity\":1,\"index\":0},\"pt\":{\"ratio\":1.0,\"polarity\":1,\"index\":0}}'"`
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,4 @@ type MeasurementSet struct {
|
||||||
StationToCompNSPaths map[string][]string // Key: StationTag, Value: NSPaths
|
StationToCompNSPaths map[string][]string // Key: StationTag, Value: NSPaths
|
||||||
CompNSPathToCompTags map[string][]string // Key: NSPaths, Value: CompTags
|
CompNSPathToCompTags map[string][]string // Key: NSPaths, Value: CompTags
|
||||||
CompTagToMeasTags map[string][]string // Key: CompTag, Value: MeasTags
|
CompTagToMeasTags map[string][]string // Key: CompTag, Value: MeasTags
|
||||||
CompNSPathToMeasTags map[string][]string // Key: NSPaths, Value: MeasTags
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Package router provides router config
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"modelRT/handler"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerDataObjectRoutes define func of register data object routes
|
||||||
|
func registerDataObjectRoutes(rg *gin.RouterGroup, middlewares ...gin.HandlerFunc) {
|
||||||
|
g := rg.Group("/data-object/")
|
||||||
|
g.Use(middlewares...)
|
||||||
|
g.GET("attribute/:token/:field", handler.DataObjectAttributeQueryHandler)
|
||||||
|
g.POST("update", handler.DataObjectAttributeUpdateHandler)
|
||||||
|
}
|
||||||
|
|
@ -27,5 +27,6 @@ func RegisterRoutes(engine *gin.Engine, clientToken string) {
|
||||||
registerDataRoutes(routeGroup)
|
registerDataRoutes(routeGroup)
|
||||||
registerMonitorRoutes(routeGroup)
|
registerMonitorRoutes(routeGroup)
|
||||||
registerComponentRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
registerComponentRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
||||||
|
registerDataObjectRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
||||||
registerAsyncTaskRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
registerAsyncTaskRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Package sql defines reusable database SQL statements.
|
||||||
|
package sql
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MeasurementCountSelect selects the number of measurements matching a
|
||||||
|
// token hierarchy and is used during token existence validation.
|
||||||
|
MeasurementCountSelect = "SELECT COUNT(*)"
|
||||||
|
|
||||||
|
// MeasurementRowsSelect selects complete measurement rows after a token has
|
||||||
|
// been parsed into its hierarchy conditions.
|
||||||
|
MeasurementRowsSelect = "SELECT m.*"
|
||||||
|
|
||||||
|
// MeasurementLimitTwo limits token resolution to two rows so callers can
|
||||||
|
// distinguish a unique match from an ambiguous token without loading all matches.
|
||||||
|
MeasurementLimitTwo = "LIMIT 2"
|
||||||
|
|
||||||
|
// MeasurementIDWhere is the GORM condition used to query a measurement by
|
||||||
|
// its database primary key.
|
||||||
|
MeasurementIDWhere = "id = ?"
|
||||||
|
|
||||||
|
// MeasurementTokenValidationQueryBase contains the common hierarchy joins
|
||||||
|
// used by all supported measurement token formats.
|
||||||
|
MeasurementTokenValidationQueryBase = MeasurementCountSelect + `
|
||||||
|
FROM measurement AS m
|
||||||
|
INNER JOIN component AS c ON c.global_uuid = m.component_uuid
|
||||||
|
INNER JOIN bay AS b ON b.bay_uuid = m.bay_uuid
|
||||||
|
INNER JOIN station AS s ON s.id = c.station_id
|
||||||
|
INNER JOIN zone AS z ON z.id = s.zone_id
|
||||||
|
INNER JOIN grid AS g ON g.id = z.grid_id`
|
||||||
|
|
||||||
|
// MeasurementSevenPartTokenWhere matches a complete token in the form
|
||||||
|
// token1.token2.token3.token4.token5.token6.token7. Token6 is validated as
|
||||||
|
// "bay" before this condition is used.
|
||||||
|
MeasurementSevenPartTokenWhere = `WHERE g.tagname = ?
|
||||||
|
AND z.tagname = ? AND s.tagname = ?
|
||||||
|
AND c.nspath = ? AND c.tag = ?
|
||||||
|
AND m.tag = ?`
|
||||||
|
|
||||||
|
// MeasurementFourPartTokenWhere matches a local measurement token in the
|
||||||
|
// form token4.token5.token6.token7. Token6 is validated as "bay" before use.
|
||||||
|
MeasurementFourPartTokenWhere = `WHERE c.nspath = ? AND c.tag = ?
|
||||||
|
AND m.tag = ?`
|
||||||
|
|
||||||
|
// MeasurementTwoPartTokenWhere matches the short measurement token format
|
||||||
|
// token4.token7 using component namespace path and measurement tag.
|
||||||
|
MeasurementTwoPartTokenWhere = `WHERE c.nspath = ? AND m.tag = ?`
|
||||||
|
|
||||||
|
// MeasurementComponentByUUID returns the component hierarchy fields needed
|
||||||
|
// to construct a measurement's canonical name and seven-part ID.
|
||||||
|
MeasurementComponentByUUID = `SELECT global_uuid, nspath,
|
||||||
|
tag, grid, zone, station FROM component
|
||||||
|
WHERE global_uuid = ? LIMIT 1`
|
||||||
|
|
||||||
|
// MeasurementGridTags returns every grid tag used to construct the first
|
||||||
|
// level of the measurement recommendation hierarchy.
|
||||||
|
MeasurementGridTags = `SELECT tagname FROM grid`
|
||||||
|
|
||||||
|
// MeasurementZoneHierarchy returns zones together with their parent grid
|
||||||
|
// tags for building the grid-to-zone recommendation mapping.
|
||||||
|
MeasurementZoneHierarchy = `SELECT zone.*,
|
||||||
|
grid.tagname AS grid_tag FROM zone
|
||||||
|
LEFT JOIN grid ON zone.grid_id = grid.id`
|
||||||
|
|
||||||
|
// MeasurementStationHierarchy returns stations together with their parent
|
||||||
|
// zone tags for building the zone-to-station recommendation mapping.
|
||||||
|
MeasurementStationHierarchy = `SELECT station.*, zone.tagname AS zone_tag
|
||||||
|
FROM station
|
||||||
|
LEFT JOIN zone ON station.zone_id = zone.id`
|
||||||
|
|
||||||
|
// MeasurementComponentHierarchy returns components together with their
|
||||||
|
// parent station tags for building station, namespace, and component mappings.
|
||||||
|
MeasurementComponentHierarchy = `SELECT component.*,
|
||||||
|
station.tagname AS station_tag
|
||||||
|
FROM component LEFT JOIN station
|
||||||
|
ON component.station_id = station.id`
|
||||||
|
|
||||||
|
// MeasurementTagHierarchy returns measurements together with their owning
|
||||||
|
// component tags for building the component-to-measurement mapping.
|
||||||
|
MeasurementTagHierarchy = `SELECT measurement.*, component.tag AS comp_tag
|
||||||
|
FROM measurement LEFT JOIN component
|
||||||
|
ON measurement.component_uuid = component.global_uuid`
|
||||||
|
)
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
package sql
|
package sql
|
||||||
|
|
||||||
// RecursiveSQL define topologic table recursive query statement
|
// RecursiveSQL define topologic table recursive query statement
|
||||||
var RecursiveSQL = `WITH RECURSIVE recursive_tree as (
|
const RecursiveSQL = `WITH RECURSIVE recursive_tree as (
|
||||||
SELECT uuid_from,uuid_to,flag
|
SELECT uuid_from,uuid_to,flag
|
||||||
FROM "topologic"
|
FROM "topologic"
|
||||||
WHERE uuid_from = ?
|
WHERE uuid_from = ?
|
||||||
|
|
@ -16,7 +16,7 @@ var RecursiveSQL = `WITH RECURSIVE recursive_tree as (
|
||||||
// RecursiveTopologicByStartSQL returns every directed edge reachable from the
|
// RecursiveTopologicByStartSQL returns every directed edge reachable from the
|
||||||
// supplied start component. It tracks the visited node path inside PostgreSQL
|
// supplied start component. It tracks the visited node path inside PostgreSQL
|
||||||
// so cycles in topologic data cannot recurse forever.
|
// so cycles in topologic data cannot recurse forever.
|
||||||
var RecursiveTopologicByStartSQL = `WITH RECURSIVE recursive_tree as (
|
const RecursiveTopologicByStartSQL = `WITH RECURSIVE recursive_tree as (
|
||||||
SELECT uuid_from, uuid_to, flag, ARRAY[uuid_from, uuid_to] AS path
|
SELECT uuid_from, uuid_to, flag, ARRAY[uuid_from, uuid_to] AS path
|
||||||
FROM "topologic"
|
FROM "topologic"
|
||||||
WHERE uuid_from = ?
|
WHERE uuid_from = ?
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,10 @@ func GetLongestCommonPrefixLength(query string, result string) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
queryRunes := []rune(query)
|
minLen := min(len(query), len(result))
|
||||||
resultRunes := []rune(result)
|
|
||||||
minLen := min(len(queryRunes), len(resultRunes))
|
|
||||||
|
|
||||||
for i := range minLen {
|
for i := range minLen {
|
||||||
if queryRunes[i] != resultRunes[i] {
|
if query[i] != result[i] {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenNanoTSStr define func to generate nanosecond timestamp string by current time
|
// GenNanoTsStr define func to generate nanosecond timestamp string by current time
|
||||||
func GenNanoTSStr() string {
|
func GenNanoTsStr() string {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nanoseconds := now.UnixNano()
|
nanoseconds := now.UnixNano()
|
||||||
timestampStr := strconv.FormatInt(nanoseconds, 10)
|
timestampStr := strconv.FormatInt(nanoseconds, 10)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue