Compare commits

..

15 Commits

Author SHA1 Message Date
douxu 72e2143cb3 feat: merge hotfix/measurement-recommend-fuzzy-offset into develop 2026-07-14 10:12:51 +08:00
douxu 8195200cea fix(recommend): handle trailing dots on complete measurements
- normalize redundant trailing dots for supported measurement formats
- validate exact measurement matches against component-specific Redis sets
- prevent terminal measurement matches from returning dot continuations
- add regression tests for offsets and exact completions
2026-07-13 15:16:30 +08:00
douxu 78f68b9c4f fix: fallback to current recommend group after hash filtering
- Return current config group recommendations when matched config candidates fail hash validation
- Return current measurement group recommendations when matched tag candidates fail hash validation
2026-07-10 17:31:56 +08:00
douxu a5e8c2ba4e fix: improve redis recommendation fuzzy fallback handling
- Preserve level continuation marker when trimming recommendation terms
- Add group fallback for config and measurement typo inputs
- Validate fuzzy exact continuation with redis set membership
- Use local nspath set for local fuzzy member checks
- Avoid returning continuation marker for exact measurement tag matches
- Add tests for fuzzy continuation and mismatch fallback behavior
2026-07-10 16:03:16 +08:00
douxu 578f805b57 fix: correct measurement recommendation fuzzy matching and hierarchy fallback
- Fix component tag suggestion writes where full paths were overwritten by local paths
- Add exact token checks for trailing-dot inputs to avoid trimming valid hierarchy levels
- Improve config/meas fallback by trimming the input prefix before filtering candidates, avoiding full-level fallback output
- Use hash-based validation for config and measurement recommendation ownership instead of long-prefix autocomplete exact queries
2026-07-10 11:02:56 +08:00
douxu 6f78d8e341 fix: improve measurement recommend validation and offset handling
- use unified response codes for measurement recommend handler
- reject recommend inputs with continuous dots as invalid params
- fix exact and continuation match offsets for recommend results
- preserve correct offsets for fallback recommend scenarios
- use hash-based confirmation for measurement token7 recommendations
- write component and bay token7 hash indexes for HEXISTS validation
- add tests for input validation, hash keys, and recommend offset cases
2026-07-09 16:31:19 +08:00
douxu 180b0f7843 test: cover initial recommend fallback inputs
- add coverage for leading-dot recommendation inputs
- verify normal recommendation inputs do not fallback to initial suggestions
2026-07-08 17:19:07 +08:00
douxu d8668afa46 refactor: simplify redis recommendation concurrency
- replace fan-in channel recommendation searches with errgroup-based execution
- return SearchResult directly from Redis recommendation helper functions
- fallback leading-dot recommendation input to initial token suggestions
- remove unused recommendLenType parameter from fuzzy member set validation
- clean up small model selection and naming issues
2026-07-08 14:41:13 +08:00
douxu f9824e2b24 fix: propagate request context through handlers
- pass c.Request.Context() to handler logs, DB calls, Redis wrappers,
and subscription operations
- avoid passing gin.Context into downstream model/database/diagram
layers
- keep async Redis backfill trace context without coupling it to request
cancellation
- add caller skip handling for facade logger output
2026-07-08 10:37:06 +08:00
douxu b53746efcd fix: improve redis recommendation fuzzy matching
- add bounded errgroup handling for attribute group suggestion initialization
- add config-level suggestions and propagate Redis/RediSearch write errors
- constrain fuzzy recommendation candidates by hierarchy-specific Redis sets
- avoid broad fallback results when concrete matches exist
- preserve full matched prefix when calculating fuzzy recommendation offset
- add tests for fuzzy recommendation offset calculation
2026-07-07 17:11:25 +08:00
douxu 305bdd4dcf fix: suppress fallback recommendations when concrete hits exist
- detect concrete recommend hits before normalizing results
- drop fallback results when any syntax path returns real candidates
- keep fallback candidates only when every path has no concrete hit
2026-07-03 15:59:53 +08:00
douxu 33eb2d9be8 feat: support nspath measurement shorthand recommendations
- add nspath-to-measurement recommend set keys and cleanup pattern
- collect nspath measurement tags when component nspath matches bay tag
- store token4.token7 autocomplete terms during measurement recommend init
- add token4.token7 recommendations alongside existing two-token paths
2026-07-02 13:49:47 +08:00
douxu 491c10e8c5 feat: add component column recommendations for config tokens
- add component as a token6 config recommendation constant
- read component table column names during startup
- bind component config recommendations to component table columns
- add full and local autocomplete terms for component column paths
2026-07-01 14:37:13 +08:00
douxu 66870c7008 fix: improve measurement recommend fallback and rune offsets
- fallback to all current-level candidates when fuzzy search has no result
- track fallback results and calculate fallback offsets from max rune length
- use rune-based prefix offsets and trimming for non-ASCII tokens
- reuse local nspath filtering for component nspath recommendations
2026-06-30 13:55:42 +08:00
douxu 367de31247 fix: flatten measurement recommend response and normalize offsets
- return a single measurement recommend payload instead of grouped payloads
- remove recommended_type from response schema and regenerated swagger docs
- normalize Redis recommend results in model layer with stable type ordering
- deduplicate trimmed recommend terms and preserve the maximum response offset
2026-06-29 14:13:58 +08:00
71 changed files with 2686 additions and 2018 deletions

View File

@ -1,15 +0,0 @@
// 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")
)

View File

@ -1,12 +0,0 @@
// 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"
)

View File

@ -4,6 +4,8 @@ package constants
const (
// DefaultScore define the default score for redissearch suggestion
DefaultScore = 1.0
// ComponentConfigKey define component config token used at token6
ComponentConfigKey = "component"
)
const (
@ -42,6 +44,9 @@ const (
// RedisSpecCompTagMeasSetKey define redis set key which store all measurement tag keys under specific component tag
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 (

View File

@ -0,0 +1,28 @@
// 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
}

View File

@ -4,174 +4,23 @@ package database
import (
"context"
"fmt"
"strings"
"time"
"modelRT/common"
"modelRT/orm"
"modelRT/sql"
"golang.org/x/sync/errgroup"
"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(sql.MeasurementIDWhere, id).
Clauses(clause.Locking{Strength: "UPDATE"}).
First(&measurement)
if result.Error != nil {
return orm.Measurement{}, result.Error
}
return measurement, nil
type ZoneWithParent struct {
orm.Zone
GridTag string `gorm:"column:grid_tag"`
}
// 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) {
measurement, _, err := QueryMeasurementByDataObjectToken(ctx, tx, token)
if err != nil {
return orm.Measurement{}, err
}
return *measurement, nil
type StationWithParent struct {
orm.Zone
ZoneTag string `gorm:"column:zone_tag"`
}
// 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) {
mSet := &orm.MeasurementSet{
GridToZoneTags: make(map[string][]string),
@ -179,6 +28,7 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
StationToCompNSPaths: make(map[string][]string),
CompNSPathToCompTags: make(map[string][]string),
CompTagToMeasTags: make(map[string][]string),
CompNSPathToMeasTags: make(map[string][]string),
}
g, gctx := errgroup.WithContext(ctx)
@ -186,7 +36,7 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
g.Go(func() error {
var grids []orm.Grid
if err := db.Raw(compactMeasurementSQL(sql.MeasurementGridTags)).Scan(&grids).Error; err != nil {
if err := db.Table("grid").Select("tagname").Scan(&grids).Error; err != nil {
return fmt.Errorf("query grids: %w", err)
}
for _, grid := range grids {
@ -202,13 +52,16 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
orm.Zone
GridTag string `gorm:"column:grid_tag"`
}
if err := db.Raw(compactMeasurementSQL(sql.MeasurementZoneHierarchy)).Scan(&zones).Error; err != nil {
if err := db.Table("zone").
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)
}
for _, zone := range zones {
mSet.AllZoneTags = append(mSet.AllZoneTags, zone.TAGNAME)
if zone.GridTag != "" {
mSet.GridToZoneTags[zone.GridTag] = append(mSet.GridToZoneTags[zone.GridTag], zone.TAGNAME)
for _, z := range zones {
mSet.AllZoneTags = append(mSet.AllZoneTags, z.TAGNAME)
if z.GridTag != "" {
mSet.GridToZoneTags[z.GridTag] = append(mSet.GridToZoneTags[z.GridTag], z.TAGNAME)
}
}
return nil
@ -219,40 +72,40 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
orm.Station
ZoneTag string `gorm:"column:zone_tag"`
}
if err := db.Raw(compactMeasurementSQL(sql.MeasurementStationHierarchy)).Scan(&stations).Error; err != nil {
if err := db.Table("station").
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)
}
for _, station := range stations {
mSet.AllStationTags = append(mSet.AllStationTags, station.TAGNAME)
if station.ZoneTag != "" {
mSet.ZoneToStationTags[station.ZoneTag] = append(mSet.ZoneToStationTags[station.ZoneTag], station.TAGNAME)
for _, s := range stations {
mSet.AllStationTags = append(mSet.AllStationTags, s.TAGNAME)
if s.ZoneTag != "" {
mSet.ZoneToStationTags[s.ZoneTag] = append(mSet.ZoneToStationTags[s.ZoneTag], s.TAGNAME)
}
}
return nil
})
g.Go(func() error {
var components []struct {
var comps []struct {
orm.Component
StationTag string `gorm:"column:station_tag"`
}
if err := db.Raw(compactMeasurementSQL(sql.MeasurementComponentHierarchy)).Scan(&components).Error; err != nil {
if err := db.Table("component").
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)
}
for _, component := range components {
mSet.AllCompNSPaths = append(mSet.AllCompNSPaths, component.NSPath)
mSet.AllCompTags = append(mSet.AllCompTags, component.Tag)
if component.StationTag != "" {
mSet.StationToCompNSPaths[component.StationTag] = append(
mSet.StationToCompNSPaths[component.StationTag],
component.NSPath,
)
for _, c := range comps {
mSet.AllCompNSPaths = append(mSet.AllCompNSPaths, c.NSPath)
mSet.AllCompTags = append(mSet.AllCompTags, c.Tag)
if c.StationTag != "" {
mSet.StationToCompNSPaths[c.StationTag] = append(mSet.StationToCompNSPaths[c.StationTag], c.NSPath)
}
if component.NSPath != "" {
mSet.CompNSPathToCompTags[component.NSPath] = append(
mSet.CompNSPathToCompTags[component.NSPath],
component.Tag,
)
if c.NSPath != "" {
mSet.CompNSPathToCompTags[c.NSPath] = append(mSet.CompNSPathToCompTags[c.NSPath], c.Tag)
}
}
return nil
@ -261,18 +114,24 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
g.Go(func() error {
var measurements []struct {
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.Raw(compactMeasurementSQL(sql.MeasurementTagHierarchy)).Scan(&measurements).Error; err != nil {
if err := db.Table("measurement").
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)
}
for _, measurement := range measurements {
mSet.AllMeasTags = append(mSet.AllMeasTags, measurement.Tag)
if measurement.CompTag != "" {
mSet.CompTagToMeasTags[measurement.CompTag] = append(
mSet.CompTagToMeasTags[measurement.CompTag],
measurement.Tag,
)
for _, m := range measurements {
mSet.AllMeasTags = append(mSet.AllMeasTags, m.Tag)
if m.CompTag != "" {
mSet.CompTagToMeasTags[m.CompTag] = append(mSet.CompTagToMeasTags[m.CompTag], m.Tag)
}
if m.CompNSPath != "" && m.CompNSPath == m.BayTag {
mSet.CompNSPathToMeasTags[m.CompNSPath] = append(mSet.CompNSPathToMeasTags[m.CompNSPath], m.Tag)
}
}
return nil

View File

@ -1,199 +0,0 @@
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())
}

View File

@ -0,0 +1,62 @@
// 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
}

View File

@ -88,7 +88,7 @@ func generateNormalData(baseValue, normalBase float64) []float64 {
func main() {
rootCtx := context.Background()
pgURI := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", "localhost", 5432, "postgres", "coslight", "develop_env")
pgURI := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", "192.168.1.101", 5432, "postgres", "coslight", "demo")
postgresDBClient, err := gorm.Open(postgres.Open(pgURI))
if err != nil {
@ -164,6 +164,7 @@ func main() {
}
datas = generateMixedData(highMin, lowMin, highBase, lowBase, baseValue, normalBase)
// log.Printf("key:%s\n datas:%v\n", key, datas)
allHigh := true
for i := highStart; i < highEnd; i++ {

View File

@ -3,7 +3,6 @@ package util
import (
"fmt"
"strings"
"modelRT/orm"
)
@ -62,7 +61,7 @@ func ProcessMeasurements(measurements []orm.Measurement) map[string]CalculationR
device, _ := ioAddress["device"].(string)
channel, _ := ioAddress["channel"].(string)
result := strings.ToLower(fmt.Sprintf("%s:%s:phasor:%s", station, device, channel))
result := fmt.Sprintf("%s:%s:phasor:%s", station, device, channel)
if measurement.EventPlan == nil {
continue
}

View File

@ -3,8 +3,6 @@ package diagram
import (
"context"
"fmt"
"strconv"
"github.com/redis/go-redis/v9"
)
@ -14,46 +12,6 @@ type RedisClient struct {
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
func NewRedisClient() *RedisClient {
return &RedisClient{

View File

@ -1,28 +0,0 @@
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)
}

View File

@ -524,10 +524,6 @@ const docTemplate = `{
" \"I_B_rms\"",
"\"I_C_rms\"]"
]
},
"recommended_type": {
"type": "string",
"example": "grid_tag"
}
}
},

View File

@ -518,10 +518,6 @@
" \"I_B_rms\"",
"\"I_C_rms\"]"
]
},
"recommended_type": {
"type": "string",
"example": "grid_tag"
}
}
},

View File

@ -86,9 +86,6 @@ definitions:
items:
type: string
type: array
recommended_type:
example: grid_tag
type: string
type: object
network.RealTimeDataPayload:
properties:

View File

@ -15,13 +15,14 @@ import (
// QueryAlertEventHandler define query alert event process API
func QueryAlertEventHandler(c *gin.Context) {
ctx := c.Request.Context()
var targetLevel constants.AlertLevel
alertManger := alert.GetAlertMangerInstance()
levelStr := c.Query("level")
level, err := strconv.Atoi(levelStr)
if err != nil {
logger.Error(c, "convert alert level string to int failed", "error", err)
logger.Error(ctx, "convert alert level string to int failed", "error", err)
resp := network.FailureResponse{
Code: -1,

View File

@ -19,15 +19,16 @@ import (
// ComponentAnchorReplaceHandler define component anchor point replace process API
func ComponentAnchorReplaceHandler(c *gin.Context) {
ctx := c.Request.Context()
var uuid, anchorName string
pgClient := database.GetPostgresDBClient()
cancelCtx, cancel := context.WithTimeout(c, 5*time.Second)
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var request network.ComponetAnchorReplaceRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "unmarshal component anchor point replace info failed", "error", err)
logger.Error(ctx, "unmarshal component anchor point replace info failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -42,7 +43,7 @@ func ComponentAnchorReplaceHandler(c *gin.Context) {
var componentInfo orm.Component
result := pgClient.WithContext(cancelCtx).Model(&orm.Component{}).Where("global_uuid = ?", uuid).Find(&componentInfo)
if result.Error != nil {
logger.Error(c, "query component detail info failed", "error", result.Error)
logger.Error(ctx, "query component detail info failed", "error", result.Error)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -54,7 +55,7 @@ func ComponentAnchorReplaceHandler(c *gin.Context) {
if result.RowsAffected == 0 {
err := fmt.Errorf("query component detail info by uuid failed:%w", errcode.ErrQueryRowZero)
logger.Error(c, "query component detail info from table is empty", "table_name", "component")
logger.Error(ctx, "query component detail info from table is empty", "table_name", "component")
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -154,7 +154,8 @@ func validateBatchImportParams(params map[string]any) bool {
func validateTestTaskParams(params map[string]any) bool {
// Test task has optional parameters, all are valid
// sleep_duration defaults to 60 seconds if not provided
fmt.Println("Test task parameters:", params)
// TODO Add more validation logic for test task parameters if needed
fmt.Println(params)
return true
}

View File

@ -13,12 +13,13 @@ import (
// AttrDeleteHandler deletes a data attribute
func AttrDeleteHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrDeleteRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
logger.Error(ctx, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -27,7 +28,7 @@ func AttrDeleteHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal attribute delete request", "error", err)
logger.Error(ctx, "failed to unmarshal attribute delete request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -35,9 +36,9 @@ func AttrDeleteHandler(c *gin.Context) {
return
}
rs := diagram.NewRedisString(c, request.AttrToken, clientToken, 10, true)
rs := diagram.NewRedisString(ctx, request.AttrToken, clientToken, 10, true)
if err := rs.GETDEL(request.AttrToken); err != nil {
logger.Error(c, "failed to delete attribute from Redis", "attr_token", request.AttrToken, "error", err)
logger.Error(ctx, "failed to delete attribute from Redis", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),

View File

@ -13,13 +13,14 @@ import (
// AttrGetHandler retrieves the value of a data attribute
func AttrGetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrGetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
logger.Error(ctx, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -28,7 +29,7 @@ func AttrGetHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal attribute get request", "error", err)
logger.Error(ctx, "failed to unmarshal attribute get request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -37,12 +38,12 @@ func AttrGetHandler(c *gin.Context) {
}
pgClient := database.GetPostgresDBClient()
tx := pgClient.Begin()
tx := pgClient.WithContext(ctx).Begin()
attrModel, err := database.ParseAttrToken(c, tx, request.AttrToken, clientToken)
attrModel, err := database.ParseAttrToken(ctx, tx, request.AttrToken, clientToken)
if err != nil {
tx.Rollback()
logger.Error(c, "failed to parse attribute token", "attr_token", request.AttrToken, "error", err)
logger.Error(ctx, "failed to parse attribute token", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),

View File

@ -13,13 +13,14 @@ import (
// AttrSetHandler sets the value of a data attribute
func AttrSetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrSetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
logger.Error(ctx, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -28,7 +29,7 @@ func AttrSetHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal attribute set request", "error", err)
logger.Error(ctx, "failed to unmarshal attribute set request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -37,9 +38,9 @@ func AttrSetHandler(c *gin.Context) {
}
// The logic for handling Redis operations directly from the handler
rs := diagram.NewRedisString(c, request.AttrToken, clientToken, 10, true)
rs := diagram.NewRedisString(ctx, request.AttrToken, clientToken, 10, true)
if err := rs.Set(request.AttrToken, request.AttrValue); err != nil {
logger.Error(c, "failed to set attribute value in Redis", "attr_token", request.AttrToken, "error", err)
logger.Error(ctx, "failed to set attribute value in Redis", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),

View File

@ -16,11 +16,12 @@ import (
// CircuitDiagramCreateHandler define circuit diagram create process API
func CircuitDiagramCreateHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
var request network.CircuitDiagramCreateRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "unmarshal circuit diagram create info failed", "error", err)
logger.Error(ctx, "unmarshal circuit diagram create info failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -32,7 +33,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
graph, err := diagram.GetGraphMap(request.PageID)
if err != nil {
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -60,7 +61,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
err = fmt.Errorf("convert uuid from string failed:%w:%w", err1, err2)
}
logger.Error(c, "format uuid from string failed", "error", err)
logger.Error(ctx, "format uuid from string failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -78,13 +79,13 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
}
// open transaction
tx := pgClient.Begin()
tx := pgClient.WithContext(ctx).Begin()
err = database.CreateTopologicIntoDB(c, tx, request.PageID, topologicCreateInfos)
err = database.CreateTopologicIntoDB(ctx, tx, request.PageID, topologicCreateInfos)
if err != nil {
tx.Rollback()
logger.Error(c, "create topologic info into DB failed", "topologic_info", topologicCreateInfos, "error", err)
logger.Error(ctx, "create topologic info into DB failed", "topologic_info", topologicCreateInfos, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -102,11 +103,11 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
}
for index, info := range request.ComponentInfos {
componentUUID, err := database.CreateComponentIntoDB(c, tx, info)
componentUUID, err := database.CreateComponentIntoDB(ctx, tx, info)
if err != nil {
tx.Rollback()
logger.Error(c, "insert component info into DB failed", "error", err)
logger.Error(ctx, "insert component info into DB failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -125,7 +126,7 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
// TODO 修复赋值问题
component, err := network.ConvertComponentCreateInfosToComponents(info)
if err != nil {
logger.Error(c, "convert component params info failed", "component_info", info, "error", err)
logger.Error(ctx, "convert component params info failed", "component_info", info, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -21,11 +21,12 @@ import (
// CircuitDiagramDeleteHandler define circuit diagram delete process API
func CircuitDiagramDeleteHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
var request network.CircuitDiagramDeleteRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "unmarshal circuit diagram del info failed", "error", err)
logger.Error(ctx, "unmarshal circuit diagram del info failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -37,7 +38,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
graph, err := diagram.GetGraphMap(request.PageID)
if err != nil {
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -65,7 +66,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
err = fmt.Errorf("convert uuid from string failed:%w:%w", err1, err2)
}
logger.Error(c, "format uuid from string failed", "error", err)
logger.Error(ctx, "format uuid from string failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -83,14 +84,14 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
}
// open transaction
tx := pgClient.Begin()
tx := pgClient.WithContext(ctx).Begin()
for _, topologicDelInfo := range topologicDelInfos {
err = database.DeleteTopologicIntoDB(c, tx, request.PageID, topologicDelInfo)
err = database.DeleteTopologicIntoDB(ctx, tx, request.PageID, topologicDelInfo)
if err != nil {
tx.Rollback()
logger.Error(c, "delete topologic info into DB failed", "topologic_info", topologicDelInfo, "error", err)
logger.Error(ctx, "delete topologic info into DB failed", "topologic_info", topologicDelInfo, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -107,7 +108,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
if err != nil {
tx.Rollback()
logger.Error(c, "delete topologic info failed", "topologic_info", topologicDelInfo, "error", err)
logger.Error(ctx, "delete topologic info failed", "topologic_info", topologicDelInfo, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -126,14 +127,14 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
}
for _, componentInfo := range request.ComponentInfos {
cancelCtx, cancel := context.WithTimeout(c, 5*time.Second)
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
globalUUID, err := uuid.FromString(componentInfo.UUID)
if err != nil {
tx.Rollback()
logger.Error(c, "format uuid from string failed", "error", err)
logger.Error(ctx, "format uuid from string failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -157,7 +158,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
}
logger.Error(c, "query component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
logger.Error(ctx, "query component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -179,7 +180,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
}
logger.Error(c, "delete component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
logger.Error(ctx, "delete component info into postgresDB failed", "component_global_uuid", componentInfo.UUID, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -24,11 +24,12 @@ import (
// @Failure 400 {object} network.FailureResponse "request process failed"
// @Router /model/diagram_load/{page_id} [get]
func CircuitDiagramLoadHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
pageID, err := strconv.ParseInt(c.Query("page_id"), 10, 64)
if err != nil {
logger.Error(c, "get pageID from url param failed", "error", err)
logger.Error(ctx, "get pageID from url param failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -43,7 +44,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
topologicInfo, err := diagram.GetGraphMap(pageID)
if err != nil {
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -62,9 +63,9 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
componentParamMap := make(map[string]any)
for _, VerticeLink := range topologicInfo.VerticeLinks {
for _, componentUUID := range VerticeLink {
component, err := database.QueryComponentByUUID(c, pgClient, componentUUID)
component, err := database.QueryComponentByUUID(ctx, pgClient, componentUUID)
if err != nil {
logger.Error(c, "get component id info from DB by uuid failed", "error", err)
logger.Error(ctx, "get component id info from DB by uuid failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -79,7 +80,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
componentParams, err := diagram.GetComponentMap(component.GlobalUUID.String())
if err != nil {
logger.Error(c, "get component data from set by uuid failed", "error", err)
logger.Error(ctx, "get component data from set by uuid failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -96,9 +97,9 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
}
rootVertexUUID := topologicInfo.RootVertex.String()
rootComponent, err := database.QueryComponentByUUID(c, pgClient, topologicInfo.RootVertex)
rootComponent, err := database.QueryComponentByUUID(ctx, pgClient, topologicInfo.RootVertex)
if err != nil {
logger.Error(c, "get component id info from DB by uuid failed", "error", err)
logger.Error(ctx, "get component id info from DB by uuid failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -113,7 +114,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
rootComponentParam, err := diagram.GetComponentMap(rootComponent.GlobalUUID.String())
if err != nil {
logger.Error(c, "get component data from set by uuid failed", "error", err)
logger.Error(ctx, "get component data from set by uuid failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -14,11 +14,12 @@ import (
// CircuitDiagramUpdateHandler define circuit diagram update process API
func CircuitDiagramUpdateHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
var request network.CircuitDiagramUpdateRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "unmarshal circuit diagram update info failed", "error", err)
logger.Error(ctx, "unmarshal circuit diagram update info failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -30,7 +31,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
graph, err := diagram.GetGraphMap(request.PageID)
if err != nil {
logger.Error(c, "get topologic data from set by pageID failed", "error", err)
logger.Error(ctx, "get topologic data from set by pageID failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -47,7 +48,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
for _, topologicLink := range request.TopologicLinks {
changeInfo, err := network.ParseUUID(topologicLink)
if err != nil {
logger.Error(c, "format uuid from string failed", "error", err)
logger.Error(ctx, "format uuid from string failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -63,14 +64,14 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
}
// open transaction
tx := pgClient.Begin()
tx := pgClient.WithContext(ctx).Begin()
for _, topologicChangeInfo := range topologicChangeInfos {
err = database.UpdateTopologicIntoDB(c, tx, request.PageID, topologicChangeInfo)
err = database.UpdateTopologicIntoDB(ctx, tx, request.PageID, topologicChangeInfo)
if err != nil {
tx.Rollback()
logger.Error(c, "update topologic info into DB failed", "topologic_info", topologicChangeInfo, "error", err)
logger.Error(ctx, "update topologic info into DB failed", "topologic_info", topologicChangeInfo, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -87,7 +88,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
if err != nil {
tx.Rollback()
logger.Error(c, "update topologic info failed", "topologic_info", topologicChangeInfo, "error", err)
logger.Error(ctx, "update topologic info failed", "topologic_info", topologicChangeInfo, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -102,9 +103,9 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
}
for index, componentInfo := range request.ComponentInfos {
componentUUID, err := database.UpdateComponentIntoDB(c, tx, componentInfo)
componentUUID, err := database.UpdateComponentIntoDB(ctx, tx, componentInfo)
if err != nil {
logger.Error(c, "udpate component info into DB failed", "error", err)
logger.Error(ctx, "udpate component info into DB failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -124,7 +125,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
// TODO 修复赋值问题
component, err := network.ConvertComponentUpdateInfosToComponents(info)
if err != nil {
logger.Error(c, "convert component params info failed", "component_info", info, "error", err)
logger.Error(ctx, "convert component params info failed", "component_info", info, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -8,6 +8,8 @@ import (
"slices"
"strings"
"github.com/gofrs/uuid"
"modelRT/common/errcode"
"modelRT/constants"
"modelRT/database"
@ -16,17 +18,17 @@ import (
"modelRT/orm"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
)
// ComponentAttributeQueryHandler define circuit diagram component attribute value query process API
func ComponentAttributeQueryHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
tokens := c.Param("tokens")
if tokens == "" {
err := fmt.Errorf("tokens is missing from the path")
logger.Error(c, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
logger.Error(ctx, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
@ -53,10 +55,10 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
dbQueryMap := make(map[string][]cacheQueryItem)
var secondaryQueryCount int
for hSetKey, items := range cacheQueryMap {
hset := diagram.NewRedisHash(c, hSetKey, 5000, false)
hset := diagram.NewRedisHash(ctx, hSetKey, 5000, false)
cacheData, err := hset.HGetAll()
if err != nil {
logger.Warn(c, "redis hgetall failed", "key", hSetKey, "err", err)
logger.Warn(ctx, "redis hgetall failed", "key", hSetKey, "err", err)
}
for _, item := range items {
if val, ok := cacheData[item.attributeName]; ok {
@ -74,9 +76,9 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
return
}
tx := pgClient.WithContext(c).Begin()
tx := pgClient.WithContext(ctx).Begin()
if tx.Error != nil {
logger.Error(c, "begin postgres transaction failed", "error", tx.Error)
logger.Error(ctx, "begin postgres transaction failed", "error", tx.Error)
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrBeginTxFailed)
payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "begin postgres database transaction failed", payload)
@ -85,9 +87,9 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
defer tx.Rollback()
allCompTags := slices.Collect(maps.Keys(dbQueryMap))
compModelMap, err := database.QueryComponentByCompTags(c, tx, allCompTags)
compModelMap, err := database.QueryComponentByCompTags(ctx, tx, allCompTags)
if err != nil {
logger.Error(c, "query component info from postgres database failed", "error", err)
logger.Error(ctx, "query component info from postgres database failed", "error", err)
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrDBQueryFailed)
payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "query component meta failed", payload)
@ -115,7 +117,7 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
tableNameMap, err := database.BatchGetProjectNames(tx, identifiers)
if err != nil {
logger.Error(c, "batch get table names from postgres database failed", "error", err)
logger.Error(ctx, "batch get table names from postgres database failed", "error", err)
fillRemainingErrors(queryResults, tokenSlice, errcode.ErrRetrieveFailed)
payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "batch get table names from postgres database failed", payload)
@ -150,10 +152,11 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
}
if err := tx.Commit().Error; err != nil {
logger.Warn(c, "postgres transaction commit failed, but returning scanned data", "error", err)
logger.Warn(ctx, "postgres transaction commit failed, but returning scanned data", "error", err)
} else {
backfillCtx := context.WithoutCancel(ctx)
for hKey, items := range redisSyncMap {
go backfillRedis(c.Copy(), hKey, items)
go backfillRedis(backfillCtx, hKey, items)
}
}

View File

@ -18,10 +18,11 @@ import (
// ComponentAttributeUpdateHandler define circuit diagram component attribute value update process API
func ComponentAttributeUpdateHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient()
var request network.ComponentAttributeUpdateInfo
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "unmarshal request params failed", "error", err)
logger.Error(ctx, "unmarshal request params failed", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
@ -54,16 +55,16 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
}
// open transaction
tx := pgClient.WithContext(c).Begin()
tx := pgClient.WithContext(ctx).Begin()
if tx.Error != nil {
logger.Error(c, "begin postgres transaction failed", "error", tx.Error)
logger.Error(ctx, "begin postgres transaction failed", "error", tx.Error)
renderRespFailure(c, constants.RespCodeServerError, "begin postgres transaction failed", nil)
return
}
compInfo, err := database.QueryComponentByCompTag(c, tx, attributeComponentTag)
compInfo, err := database.QueryComponentByCompTag(ctx, tx, attributeComponentTag)
if err != nil {
logger.Error(c, "query component info by component tag failed", "error", err, "tag", attributeComponentTag)
logger.Error(ctx, "query component info by component tag failed", "error", err, "tag", attributeComponentTag)
for _, attribute := range request.AttributeConfigs {
if _, exists := updateResults[attribute.AttributeToken]; !exists {
@ -139,7 +140,7 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
}
for key, items := range redisUpdateMap {
hset := diagram.NewRedisHash(c, key, 5000, false)
hset := diagram.NewRedisHash(ctx, key, 5000, false)
fields := make(map[string]any, len(items))
for _, item := range items {
@ -147,7 +148,7 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
}
if err := hset.SetRedisHashByMap(fields); err != nil {
logger.Error(c, "batch sync redis failed", "hash_key", key, "error", err)
logger.Error(ctx, "batch sync redis failed", "hash_key", key, "error", err)
for _, item := range items {
if _, exists := updateResults[item.token]; exists {

View File

@ -1,197 +0,0 @@
// 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
}

View File

@ -1,115 +0,0 @@
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")
}

View File

@ -1,166 +0,0 @@
// 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)
}

View File

@ -41,11 +41,12 @@ var linkSetConfigs = map[int]linkSetConfig{
// DiagramNodeLinkHandler defines the diagram node link process api
func DiagramNodeLinkHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.DiagramNodeLinkRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
logger.Error(ctx, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -54,7 +55,7 @@ func DiagramNodeLinkHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal diagram node process request", "error", err)
logger.Error(ctx, "failed to unmarshal diagram node process request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -68,9 +69,9 @@ func DiagramNodeLinkHandler(c *gin.Context) {
nodeID := request.NodeID
nodeLevel := request.NodeLevel
action := request.Action
prevNodeInfo, currNodeInfo, err := database.QueryNodeInfoByID(c, pgClient, nodeID, nodeLevel)
prevNodeInfo, currNodeInfo, err := database.QueryNodeInfoByID(ctx, pgClient, nodeID, nodeLevel)
if err != nil {
logger.Error(c, "failed to query diagram node info by nodeID and level from postgres", "node_id", nodeID, "level", nodeLevel, "error", err)
logger.Error(ctx, "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{
Code: http.StatusBadRequest,
@ -84,8 +85,8 @@ func DiagramNodeLinkHandler(c *gin.Context) {
return
}
prevLinkSet, currLinkSet := generateLinkSet(c, nodeLevel, prevNodeInfo)
err = processLinkSetData(c, action, nodeLevel, prevLinkSet, currLinkSet, prevNodeInfo, currNodeInfo)
prevLinkSet, currLinkSet := generateLinkSet(ctx, nodeLevel, prevNodeInfo)
err = processLinkSetData(ctx, action, nodeLevel, prevLinkSet, currLinkSet, prevNodeInfo, currNodeInfo)
if err != nil {
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -99,7 +100,7 @@ func DiagramNodeLinkHandler(c *gin.Context) {
return
}
logger.Info(c, "process diagram node link success", "node_id", nodeID, "level", nodeLevel, "action", request.Action)
logger.Info(ctx, "process diagram node link success", "node_id", nodeID, "level", nodeLevel, "action", request.Action)
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,

View File

@ -16,11 +16,12 @@ import (
// QueryHistoryDataHandler define query history data process API
func QueryHistoryDataHandler(c *gin.Context) {
ctx := c.Request.Context()
token := c.Query("token")
beginStr := c.Query("begin")
begin, err := strconv.Atoi(beginStr)
if err != nil {
logger.Error(c, "convert begin param from string to int failed", "error", err)
logger.Error(ctx, "convert begin param from string to int failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
@ -31,7 +32,7 @@ func QueryHistoryDataHandler(c *gin.Context) {
endStr := c.Query("end")
end, err := strconv.Atoi(endStr)
if err != nil {
logger.Error(c, "convert end param from string to int failed", "error", err)
logger.Error(ctx, "convert end param from string to int failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,

View File

@ -15,13 +15,14 @@ import (
// MeasurementGetHandler define measurement query API
func MeasurementGetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementGetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
logger.Error(ctx, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -30,7 +31,7 @@ func MeasurementGetHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal measurement get request", "error", err)
logger.Error(ctx, "failed to unmarshal measurement get request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -38,10 +39,10 @@ func MeasurementGetHandler(c *gin.Context) {
return
}
zset := diagram.NewRedisZSet(c, request.MeasurementToken, 0, false)
zset := diagram.NewRedisZSet(ctx, request.MeasurementToken, 0, false)
points, err := zset.ZRANGE(request.MeasurementToken, 0, -1)
if err != nil {
logger.Error(c, "failed to get measurement data from redis", "measurement_token", request.MeasurementToken, "error", err)
logger.Error(ctx, "failed to get measurement data from redis", "measurement_token", request.MeasurementToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusInternalServerError,
Msg: err.Error(),
@ -54,9 +55,9 @@ func MeasurementGetHandler(c *gin.Context) {
}
pgClient := database.GetPostgresDBClient()
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, request.MeasurementID)
measurementInfo, err := database.QueryMeasurementByID(ctx, pgClient, request.MeasurementID)
if err != nil {
logger.Error(c, "failed to query measurement by id", "measurement_id", request.MeasurementID, "error", err)
logger.Error(ctx, "failed to query measurement by id", "measurement_id", request.MeasurementID, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),

View File

@ -2,11 +2,13 @@
package handler
import (
"errors"
"strings"
"modelRT/constants"
"modelRT/logger"
"modelRT/model"
"modelRT/network"
"modelRT/util"
"net/http"
"github.com/gin-gonic/gin"
)
@ -43,79 +45,81 @@ import (
//
// @Router /measurement/recommend [get]
func MeasurementRecommendHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementRecommendRequest
if err := c.ShouldBindQuery(&request); err != nil {
logger.Error(c, "failed to bind measurement recommend request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
logger.Error(ctx, "failed to bind measurement recommend request", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
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
}
recommendResults := model.RedisSearchRecommend(ctx, request.Input)
payload := network.MeasurementRecommendPayload{
Input: request.Input,
RecommendedList: make([]string, 0),
}
seen := make(map[string]struct{})
orderedResults := orderedRecommendResults(recommendResults)
recommendResults := model.RedisSearchRecommend(c, request.Input)
payloads := make([]network.MeasurementRecommendPayload, 0, len(recommendResults))
for _, recommendResult := range recommendResults {
for _, recommendResult := range orderedResults {
if recommendResult.Err != nil {
err := recommendResult.Err
logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusInternalServerError,
Msg: err.Error(),
Payload: map[string]any{
"input": request.Input,
},
logger.Error(ctx, "failed to get recommend data from redis", "input", request.Input, "error", err)
renderRespFailure(c, constants.RespCodeServerError, err.Error(), map[string]any{
"input": request.Input,
})
return
}
var finalOffset int
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
if recommendResult.Offset > payload.Offset {
payload.Offset = recommendResult.Offset
}
resultRecommends := make([]string, 0, len(recommends))
seen := make(map[string]struct{})
for _, recommend := range recommends {
recommendTerm := recommend[finalOffset:]
if len(recommendTerm) != 0 {
if _, exists := seen[recommendTerm]; !exists {
seen[recommendTerm] = struct{}{}
resultRecommends = append(resultRecommends, recommendTerm)
}
}
}
payloads = append(payloads, network.MeasurementRecommendPayload{
Input: request.Input,
Offset: finalOffset,
RecommendType: recommendResult.RecommendType.String(),
RecommendedList: resultRecommends,
})
}
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,
Msg: "success",
Payload: &payloads,
})
for _, recommendResult := range orderedResults {
for _, recommend := range recommendResult.QueryDatas {
if _, exists := seen[recommend]; !exists {
seen[recommend] = struct{}{}
payload.RecommendedList = append(payload.RecommendedList, recommend)
}
}
}
renderRespSuccess(c, constants.RespCodeSuccess, "success", payload)
}
func orderedRecommendResults(recommendResults map[string]model.SearchResult) []model.SearchResult {
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
}

View File

@ -0,0 +1,44 @@
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")
}
})
}
}

View File

@ -18,11 +18,12 @@ import (
// MeasurementLinkHandler defines the measurement link process api
func MeasurementLinkHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementLinkRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := common.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
logger.Error(ctx, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -31,7 +32,7 @@ func MeasurementLinkHandler(c *gin.Context) {
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal measurement process request", "error", err)
logger.Error(ctx, "failed to unmarshal measurement process request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -44,9 +45,9 @@ func MeasurementLinkHandler(c *gin.Context) {
pgClient := database.GetPostgresDBClient()
measurementID := request.MeasurementID
action := request.Action
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, measurementID)
measurementInfo, err := database.QueryMeasurementByID(ctx, pgClient, measurementID)
if err != nil {
logger.Error(c, "failed to query measurement info by measurement id from postgres", "meauserement_id", measurementID, "error", err)
logger.Error(ctx, "failed to query measurement info by measurement id from postgres", "meauserement_id", measurementID, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -59,9 +60,9 @@ func MeasurementLinkHandler(c *gin.Context) {
return
}
componentInfo, err := database.QueryComponentByUUID(c, pgClient, measurementInfo.ComponentUUID)
componentInfo, err := database.QueryComponentByUUID(ctx, pgClient, measurementInfo.ComponentUUID)
if err != nil {
logger.Error(c, "failed to query component info by component uuid from postgres", "component_uuid", measurementInfo.ComponentUUID, "error", err)
logger.Error(ctx, "failed to query component info by component uuid from postgres", "component_uuid", measurementInfo.ComponentUUID, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
@ -74,9 +75,9 @@ func MeasurementLinkHandler(c *gin.Context) {
return
}
allMeasSet := diagram.NewRedisSet(c, constants.RedisAllMeasTagSetKey, 0, false)
allMeasSet := diagram.NewRedisSet(ctx, constants.RedisAllMeasTagSetKey, 0, false)
compMeasLinkKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, componentInfo.Tag)
compMeasLinkSet := diagram.NewRedisSet(c, compMeasLinkKey, 0, false)
compMeasLinkSet := diagram.NewRedisSet(ctx, compMeasLinkKey, 0, false)
switch action {
case constants.SearchLinkAddAction:
@ -84,18 +85,18 @@ func MeasurementLinkHandler(c *gin.Context) {
err2 := compMeasLinkSet.SADD(measurementInfo.Tag)
err = processActionError(err1, err2, action)
if err != nil {
logger.Error(c, "add measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
logger.Error(ctx, "add measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
}
case constants.SearchLinkDelAction:
err1 := allMeasSet.SREM(measurementInfo.Tag)
err2 := compMeasLinkSet.SREM(measurementInfo.Tag)
err = processActionError(err1, err2, action)
if err != nil {
logger.Error(c, "del measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
logger.Error(ctx, "del measurement link process operation failed", "measurement_id", measurementID, "action", action, "error", err)
}
default:
err = common.ErrUnsupportedLinkAction
logger.Error(c, "unsupport measurement link process action", "measurement_id", measurementID, "action", action, "error", err)
logger.Error(ctx, "unsupport measurement link process action", "measurement_id", measurementID, "action", action, "error", err)
}
if err != nil {
@ -110,7 +111,7 @@ func MeasurementLinkHandler(c *gin.Context) {
return
}
logger.Info(c, "process measurement link success", "measurement_id", measurementID, "action", request.Action)
logger.Info(ctx, "process measurement link success", "measurement_id", measurementID, "action", request.Action)
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,

View File

@ -35,27 +35,28 @@ var pullUpgrader = websocket.Upgrader{
// @Tags RealTime Component Websocket
// @Router /monitors/data/realtime/stream/:clientID [get]
func PullRealTimeDataHandler(c *gin.Context) {
requestCtx := c.Request.Context()
clientID := c.Param("clientID")
if clientID == "" {
err := fmt.Errorf("clientID is missing from the path")
logger.Error(c, "query clientID from path failed", "error", err, "url", c.Request.RequestURI)
logger.Error(requestCtx, "query clientID from path failed", "error", err, "url", c.Request.RequestURI)
renderWSRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
conn, err := pullUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(c, "upgrade http protocol to websocket protocol failed", "error", err)
logger.Error(requestCtx, "upgrade http protocol to websocket protocol failed", "error", err)
renderWSRespFailure(c, constants.RespCodeServerError, err.Error(), nil)
return
}
defer conn.Close()
ctx, cancel := context.WithCancel(c.Request.Context())
ctx, cancel := context.WithCancel(requestCtx)
defer cancel()
conn.SetCloseHandler(func(code int, text string) error {
logger.Info(c.Request.Context(), "websocket processor shutdown trigger",
logger.Info(requestCtx, "websocket processor shutdown trigger",
"clientID", clientID, "code", code, "reason", text)
// call cancel to notify other goroutines to stop working

View File

@ -60,10 +60,11 @@ var wsUpgrader = websocket.Upgrader{
//
// @Router /data/realtime [get]
func QueryRealTimeDataHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.RealTimeQueryRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal real time query request", "error", err)
logger.Error(ctx, "failed to unmarshal real time query request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
@ -73,7 +74,7 @@ func QueryRealTimeDataHandler(c *gin.Context) {
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(c, "upgrade http protocol to websocket protocal failed", "error", err)
logger.Error(ctx, "upgrade http protocol to websocket protocal failed", "error", err)
return
}
defer conn.Close()
@ -87,29 +88,29 @@ func QueryRealTimeDataHandler(c *gin.Context) {
case data := <-transportChannel:
respByte, err := jsoniter.Marshal(data)
if err != nil {
logger.Error(c, "marshal real time data to bytes failed", "error", err)
logger.Error(ctx, "marshal real time data to bytes failed", "error", err)
continue
}
err = conn.WriteMessage(websocket.TextMessage, respByte)
if err != nil {
logger.Error(c, "write message to websocket connection failed", "error", err)
logger.Error(ctx, "write message to websocket connection failed", "error", err)
continue
}
case <-closeChannel:
logger.Info(c, "data receiving goroutine has been closed")
logger.Info(ctx, "data receiving goroutine has been closed")
// TODO 优化时间控制
deadline := time.Now().Add(5 * time.Second)
err := conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "the session ended normally"), deadline)
if err != nil {
logger.Error(c, "sending close control message failed", "error", err)
logger.Error(ctx, "sending close control message failed", "error", err)
}
// gracefully close session processing
err = conn.Close()
if err != nil {
logger.Error(c, "websocket conn closed failed", "error", err)
logger.Error(ctx, "websocket conn closed failed", "error", err)
}
logger.Info(c, "websocket connection closed successfully.")
logger.Info(ctx, "websocket connection closed successfully.")
}
}
}

View File

@ -17,9 +17,10 @@ var upgrader = websocket.Upgrader{
// RealTimeDataReceivehandler define real time data receive and process API
func RealTimeDataReceivehandler(c *gin.Context) {
ctx := c.Request.Context()
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(c, "upgrade http protocol to websocket protocal failed", "error", err)
logger.Error(ctx, "upgrade http protocol to websocket protocal failed", "error", err)
return
}
defer conn.Close()
@ -27,17 +28,17 @@ func RealTimeDataReceivehandler(c *gin.Context) {
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
logger.Error(c, "read message from websocket connection failed", "error", err)
logger.Error(ctx, "read message from websocket connection failed", "error", err)
respByte := processResponse(-1, "read message from websocket connection failed", nil)
if len(respByte) == 0 {
logger.Error(c, "process message from byte failed", "error", err)
logger.Error(ctx, "process message from byte failed", "error", err)
continue
}
err = conn.WriteMessage(messageType, respByte)
if err != nil {
logger.Error(c, "write message to websocket connection failed", "error", err)
logger.Error(ctx, "write message to websocket connection failed", "error", err)
continue
}
continue
@ -46,17 +47,17 @@ func RealTimeDataReceivehandler(c *gin.Context) {
var request network.RealTimeDataReceiveRequest
err = jsoniter.Unmarshal([]byte(p), &request)
if err != nil {
logger.Error(c, "unmarshal message from byte failed", "error", err)
logger.Error(ctx, "unmarshal message from byte failed", "error", err)
respByte := processResponse(-1, "unmarshal message from byte failed", nil)
if len(respByte) == 0 {
logger.Error(c, "process message from byte failed", "error", err)
logger.Error(ctx, "process message from byte failed", "error", err)
continue
}
err = conn.WriteMessage(messageType, respByte)
if err != nil {
logger.Error(c, "write message to websocket connection failed", "error", err)
logger.Error(ctx, "write message to websocket connection failed", "error", err)
continue
}
continue
@ -70,13 +71,13 @@ func RealTimeDataReceivehandler(c *gin.Context) {
}
respByte := processResponse(0, "success", payload)
if len(respByte) == 0 {
logger.Error(c, "process message from byte failed", "error", err)
logger.Error(ctx, "process message from byte failed", "error", err)
continue
}
err = conn.WriteMessage(messageType, respByte)
if err != nil {
logger.Error(c, "write message to websocket connection failed", "error", err)
logger.Error(ctx, "write message to websocket connection failed", "error", err)
continue
}
}

View File

@ -77,12 +77,13 @@ func init() {
//
// @Router /monitors/data/subscriptions [post]
func RealTimeSubHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.RealTimeSubRequest
var subAction string
var clientID string
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal real time query request", "error", err)
logger.Error(ctx, "failed to unmarshal real time query request", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
@ -91,7 +92,7 @@ func RealTimeSubHandler(c *gin.Context) {
subAction = request.Action
id, err := uuid.NewV4()
if err != nil {
logger.Error(c, "failed to generate client id", "error", err)
logger.Error(ctx, "failed to generate client id", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return
}
@ -114,9 +115,9 @@ func RealTimeSubHandler(c *gin.Context) {
switch subAction {
case constants.SubStartAction:
results, err := globalSubState.CreateConfig(c, tx, clientID, request.Measurements)
results, err := globalSubState.CreateConfig(ctx, tx, clientID, request.Measurements)
if err != nil {
logger.Error(c, "create real time data subscription config failed", "error", err)
logger.Error(ctx, "create real time data subscription config failed", "error", err)
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
ClientID: clientID,
TargetResults: results,
@ -130,9 +131,9 @@ func RealTimeSubHandler(c *gin.Context) {
})
return
case constants.SubStopAction:
results, err := globalSubState.RemoveTargets(c, clientID, request.Measurements)
results, err := globalSubState.RemoveTargets(ctx, clientID, request.Measurements)
if err != nil {
logger.Error(c, "remove target to real time data subscription config failed", "error", err)
logger.Error(ctx, "remove target to real time data subscription config failed", "error", err)
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
ClientID: clientID,
TargetResults: results,
@ -146,9 +147,9 @@ func RealTimeSubHandler(c *gin.Context) {
})
return
case constants.SubAppendAction:
results, err := globalSubState.AppendTargets(c, tx, clientID, request.Measurements)
results, err := globalSubState.AppendTargets(ctx, tx, clientID, request.Measurements)
if err != nil {
logger.Error(c, "append target to real time data subscription config failed", "error", err)
logger.Error(ctx, "append target to real time data subscription config failed", "error", err)
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
ClientID: clientID,
TargetResults: results,
@ -162,9 +163,9 @@ func RealTimeSubHandler(c *gin.Context) {
})
return
case constants.SubUpdateAction:
results, err := globalSubState.UpdateTargets(c, tx, clientID, request.Measurements)
results, err := globalSubState.UpdateTargets(ctx, tx, clientID, request.Measurements)
if err != nil {
logger.Error(c, "update target to real time data subscription config failed", "error", err)
logger.Error(ctx, "update target to real time data subscription config failed", "error", err)
renderRespFailure(c, constants.RespCodeFailed, err.Error(), network.RealTimeSubPayload{
ClientID: clientID,
TargetResults: results,
@ -179,7 +180,7 @@ func RealTimeSubHandler(c *gin.Context) {
return
default:
err := fmt.Errorf("%w: request action is %s", common.ErrUnsupportedSubAction, request.Action)
logger.Error(c, "unsupported action of real time data subscription request", "error", err)
logger.Error(ctx, "unsupported action of real time data subscription request", "error", err)
requestTargetsCount := processRealTimeRequestCount(request.Measurements)
results := processRealTimeRequestTargets(request.Measurements, requestTargetsCount, constants.CodeUnsupportSubOperation, err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), network.RealTimeSubPayload{

View File

@ -1,50 +0,0 @@
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)
}

View File

@ -18,6 +18,8 @@ type facade struct {
_logger *zap.Logger
}
const facadeCallerSkip = 2
// Debug define facade func of debug level log
func Debug(ctx context.Context, msg string, kv ...any) {
logFacade().log(ctx, zapcore.DebugLevel, msg, kv...)
@ -39,17 +41,16 @@ func Error(ctx context.Context, msg string, kv ...any) {
}
func (f *facade) log(ctx context.Context, lvl zapcore.Level, msg string, kv ...any) {
f.logSkip(ctx, lvl, 0, msg, kv...)
f.logSkip(ctx, lvl, 1, msg, kv...)
}
func (f *facade) logSkip(ctx context.Context, lvl zapcore.Level, extraSkip int, msg string, kv ...any) {
caller := resolveLoggerCaller(extraSkip)
fields := makeLogFieldsWithCaller(ctx, caller, kv...)
ce := f._logger.Check(lvl, msg)
if ce == nil {
return
fields := makeLogFieldsSkip(ctx, extraSkip, kv...)
logger := f._logger
if extraSkip > 0 {
logger = logger.WithOptions(zap.AddCallerSkip(extraSkip))
}
setCheckedEntryCaller(ce, caller)
ce := logger.Check(lvl, msg)
ce.Write(fields...)
}
@ -71,7 +72,7 @@ func InfoSkip(ctx context.Context, extraSkip int, msg string, kv ...any) {
func logFacade() *facade {
fOnce.Do(func() {
f = &facade{
_logger: GetLoggerInstance(),
_logger: GetLoggerInstance().WithOptions(zap.AddCallerSkip(facadeCallerSkip)),
}
})
return f

View File

@ -50,12 +50,12 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql
// get gorm exec sql and rows affected
sql, rows := fc()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
ErrorSkip(ctx, 0, "SQL ERROR", "sql", sql, "rows", rows, "dur(ms)", duration)
ErrorSkip(ctx, 1, "SQL ERROR", "sql", sql, "rows", rows, "dur(ms)", duration)
return
}
if duration > l.SlowThreshold.Milliseconds() {
WarnSkip(ctx, 0, "SQL SLOW", "sql", sql, "rows", rows, "dur(ms)", duration)
WarnSkip(ctx, 1, "SQL SLOW", "sql", sql, "rows", rows, "dur(ms)", duration)
} else {
InfoSkip(ctx, 0, "SQL INFO", "sql", sql, "rows", rows, "dur(ms)", duration)
InfoSkip(ctx, 1, "SQL INFO", "sql", sql, "rows", rows, "dur(ms)", duration)
}
}

View File

@ -5,7 +5,6 @@ import (
"context"
"path"
"runtime"
"strings"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
@ -42,13 +41,8 @@ func (l *logger) Error(msg string, kv ...any) {
}
func (l *logger) log(lvl zapcore.Level, msg string, kv ...any) {
caller := resolveLoggerCaller(0)
fields := makeLogFieldsWithCaller(l.ctx, caller, kv...)
fields := makeLogFields(l.ctx, kv...)
ce := l._logger.Check(lvl, msg)
if ce == nil {
return
}
setCheckedEntryCaller(ce, caller)
ce.Write(fields...)
}
@ -57,10 +51,6 @@ func makeLogFields(ctx context.Context, 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 {
kv = append(kv, "unknown")
}
@ -70,7 +60,8 @@ func makeLogFieldsWithCaller(ctx context.Context, caller loggerCaller, kv ...any
spanID := spanCtx.SpanID().String()
kv = append(kv, "traceID", traceID, "spanID", spanID)
kv = append(kv, "func", caller.funcName, "file", caller.shortFile, "line", caller.line)
funcName, file, line := getLoggerCallerInfoSkip(extraSkip)
kv = append(kv, "func", funcName, "file", file, "line", line)
fields := make([]zap.Field, 0, len(kv)/2)
for i := 0; i < len(kv); i += 2 {
key := kv[i].(string)
@ -104,59 +95,13 @@ func getLoggerCallerInfo() (funcName, file string, line int) {
// getLoggerCallerInfoSkip returns caller info with additional skip frames beyond the standard depth.
func getLoggerCallerInfoSkip(extraSkip int) (funcName, file string, line int) {
caller := resolveLoggerCaller(extraSkip)
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 {
pc, file, line, ok := runtime.Caller(4 + extraSkip)
if !ok {
return
}
entry.Entry.Caller = zapcore.EntryCaller{
Defined: true,
PC: caller.pc,
File: caller.fullFile,
Line: caller.line,
}
file = path.Base(file)
funcName = runtime.FuncForPC(pc).Name()
return
}
// New returns a logger bound to ctx. Trace fields (traceID, spanID) are extracted

12
main.go
View File

@ -235,6 +235,18 @@ func main() {
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)
if err != nil {
logger.Error(ctx, "load topologic info from postgres failed", "error", err)

View File

@ -12,6 +12,7 @@ import (
"modelRT/orm"
"github.com/RediSearch/redisearch-go/v2/redisearch"
"golang.org/x/sync/errgroup"
"gorm.io/gorm"
)
@ -20,6 +21,13 @@ type columnParam struct {
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
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
@ -38,6 +46,7 @@ func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFul
return nil
}
jobs := make([]attributeGroupRecommendJob, 0)
for _, tableName := range tableNames {
var records []map[string]any
err := db.Table(tableName).Find(&records).Error
@ -102,13 +111,27 @@ func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFul
AttributeType: attributeType,
AttributeGroup: attributeGroup,
}
go storeAttributeGroup(ctx, attrSet, fullPath, isLocalfullPath, columnParam)
jobs = append(jobs, attributeGroupRecommendJob{
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) {
func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, fullPath string, isLocalFullPath string, colParams columnParam) error {
rdb := diagram.GetRedisClientInstance()
pipe := rdb.Pipeline()
@ -121,18 +144,25 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
attrbutesGroups := make([]any, 0, len(colParams.AttributeGroup)*2)
attributeGroupKey := fmt.Sprintf("%s_%s", attributeSet.CompTag, colParams.AttributeType)
sug := make([]redisearch.Suggestion, 0, len(colParams.AttributeGroup)*4)
sug := make([]redisearch.Suggestion, 0, len(colParams.AttributeGroup)*2+2)
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 {
attrbutesGroups = append(attrbutesGroups, attrName, attrValue)
attrNameMembers = append(attrNameMembers, attrName)
// 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)
sug = append(sug, redisearch.Suggestion{
Term: measTerm,
@ -140,11 +170,9 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
})
// add redis fuzzy search suggestion for token4-token7 type
configTerm = fmt.Sprintf("%s.%s", isLocalFullPath, colParams.AttributeType)
sug = append(sug, redisearch.Suggestion{
Term: configTerm,
Score: constants.DefaultScore,
})
if isLocalFullPath == "" {
continue
}
measTerm = fmt.Sprintf("%s.%s.%s", isLocalFullPath, colParams.AttributeType, attrName)
sug = append(sug, redisearch.Suggestion{
@ -163,11 +191,24 @@ func storeAttributeGroup(ctx context.Context, attributeSet orm.AttributeSet, ful
}
if len(sug) > 0 {
ac.AddTerms(sug...)
if err := ac.AddTerms(sug...); err != nil {
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)
if err != nil {
logger.Error(ctx, "init component attribute group recommend content failed", "error", err)
logger.Error(ctx, "init component attribute group recommend content failed",
"component_tag", attributeSet.CompTag,
"attribute_type", colParams.AttributeType,
"error", err,
)
return fmt.Errorf("init component attribute group recommend content: %w", err)
}
return nil
}

View File

@ -0,0 +1,91 @@
// 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)
}

View File

@ -0,0 +1,22 @@
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)
}
}

View File

@ -1,57 +0,0 @@
// 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))
}
}

View File

@ -1,101 +0,0 @@
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)
})
}
}

View File

@ -1,78 +0,0 @@
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)
}
}

View File

@ -1,40 +0,0 @@
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)
}
}

View File

@ -22,6 +22,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
compTagToFullPath := make(map[string]string)
isLocalCompTagToFullPath := make(map[string]string)
var allErrs []error
zoneToGridPath := make(map[string]string)
for gridTag, zoneTags := range measSet.GridToZoneTags {
@ -62,12 +63,21 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
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)
gridSug := util.MapSlice(measSet.AllGridTags, func(gridTag string) redisearch.Suggestion {
return redisearch.Suggestion{Term: gridTag, Score: constants.DefaultScore}
})
ac.AddTerms(gridSug...)
safeAddTerms(gridSug)
safeSAdd(constants.RedisAllZoneSetKey, measSet.AllZoneTags)
safeSAdd(constants.RedisAllStationSetKey, measSet.AllStationTags)
@ -83,7 +93,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
return redisearch.Suggestion{Term: fmt.Sprintf("%s.%s", gridTag, zoneTag), Score: constants.DefaultScore}
})
safeSAdd(fmt.Sprintf(constants.RedisSpecGridZoneSetKey, gridTag), zoneTags)
ac.AddTerms(sug...)
safeAddTerms(sug)
}
// building the zone -> stations hierarchy
@ -101,7 +111,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
})
safeSAdd(fmt.Sprintf(constants.RedisSpecZoneStationSetKey, zoneTag), stationTags)
ac.AddTerms(sug...)
safeAddTerms(sug)
}
// building the station -> component nspaths hierarchy
@ -122,7 +132,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
sug = append(sug, redisearch.Suggestion{Term: nsPath, Score: constants.DefaultScore})
}
safeSAdd(fmt.Sprintf(constants.RedisSpecStationCompNSPATHSetKey, stationTag), compNSPaths)
ac.AddTerms(sug...)
safeAddTerms(sug)
}
// building the component nspath -> component tags hierarchy
@ -136,20 +146,17 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
}
for _, compTag := range compTags {
fullPath := fmt.Sprintf("%s.%s.%s", parentPath, compNSPath, compTag)
compTagToFullPath[compTag] = fullPath
fullPath = fmt.Sprintf("%s.%s", compNSPath, compTag)
isLocalCompTagToFullPath[compTag] = fullPath
fullTerm, localTerm := compTagSuggestionTerms(parentPath, compNSPath, compTag)
compTagToFullPath[compTag] = fullTerm
isLocalCompTagToFullPath[compTag] = localTerm
// add redis fuzzy search suggestion for token1-token7 type
term := fullPath
sug = append(sug, redisearch.Suggestion{Term: term, Score: constants.DefaultScore})
sug = append(sug, redisearch.Suggestion{Term: fullTerm, Score: constants.DefaultScore})
// add redis fuzzy search suggestion for token4-token7 type
term = fmt.Sprintf("%s.%s", compNSPath, compTag)
sug = append(sug, redisearch.Suggestion{Term: term, Score: constants.DefaultScore})
sug = append(sug, redisearch.Suggestion{Term: localTerm, Score: constants.DefaultScore})
}
safeSAdd(fmt.Sprintf(constants.RedisSpecCompNSPathCompTagSetKey, compNSPath), compTags)
ac.AddTerms(sug...)
safeAddTerms(sug)
}
// building the component tag -> measurement tags hierarchy
@ -184,10 +191,25 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
sug = append(sug, redisearch.Suggestion{Term: measTerm, Score: constants.DefaultScore})
}
safeSAdd(fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, compTag), measTags)
ac.AddTerms(sug...)
if len(measTags) > 0 {
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)
if execErr != nil {
logger.Error(ctx, "pipeline execution failed", "error", execErr)
@ -211,3 +233,7 @@ func TraverseMeasurementGroupTables(ctx context.Context, measSet orm.Measurement
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)
}

View File

@ -0,0 +1,21 @@
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)
}
}

View File

@ -1,35 +0,0 @@
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)
}

View File

@ -245,18 +245,24 @@ func GenerateMeasureIdentifier(source map[string]any) (string, error) {
if !ok {
return "", fmt.Errorf("Power104: missing packet field")
}
packet, err := integerJSONValue(packetVal)
if err != nil {
return "", fmt.Errorf("Power104:invalid packet format: %w", err)
var packet int
switch v := packetVal.(type) {
case int:
packet = v
default:
return "", fmt.Errorf("Power104:invalid packet format")
}
offsetVal, ok := ioAddress["offset"]
if !ok {
return "", fmt.Errorf("Power104:missing offset field")
}
offset, err := integerJSONValue(offsetVal)
if err != nil {
return "", fmt.Errorf("Power104:invalid offset format: %w", err)
var offset int
switch v := offsetVal.(type) {
case int:
offset = v
default:
return "", fmt.Errorf("Power104:invalid offset format")
}
return concatP104WithPlus(station, packet, offset), nil
default:

View File

@ -1,3 +1,4 @@
// Package model define model struct of model runtime service
package model
import (

View File

@ -48,11 +48,12 @@ func CleanupRecommendRedisCache(ctx context.Context) error {
}
patterns := []string{
"*_zone_tag_keys", // correspond RedisSpecGridZoneSetKey
"*_station_tag_keys", // correspond RedisSpecZoneStationSetKey
"*_component_nspath_keys", // correspond RedisSpecStationCompNSPATHSetKey
"*_component_tag_keys", // correspond RedisSpecCompNSPathCompTagSetKey
"*_measurement_tag_keys", // correspond RedisSpecCompTagMeasSetKey
"*_zone_tag_keys", // correspond RedisSpecGridZoneSetKey
"*_station_tag_keys", // correspond RedisSpecZoneStationSetKey
"*_component_nspath_keys", // correspond RedisSpecStationCompNSPATHSetKey
"*_component_tag_keys", // correspond RedisSpecCompNSPathCompTagSetKey
"*_measurement_tag_keys", // correspond RedisSpecCompTagMeasSetKey
"*_nspath_measurement_tag_keys", // correspond RedisSpecCompNSPathMeasSetKey
}
for _, pattern := range patterns {

15
model/recommend_hash.go Normal file
View File

@ -0,0 +1,15 @@
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

View File

@ -0,0 +1,945 @@
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)
}
})
}
}

View File

@ -26,7 +26,6 @@ type WSResponse struct {
type MeasurementRecommendPayload struct {
Input string `json:"input" example:"transformfeeder1_220."`
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\"]"`
}

View File

@ -117,7 +117,7 @@ func (a *AsyncTask) IsFailed() bool {
return a.Status == AsyncTaskStatusFailed
}
// IsValidAsyncTaskType checks if the task type is valid
// IsValidTaskType checks if the task type is valid
func IsValidAsyncTaskType(taskType string) bool {
switch AsyncTaskType(taskType) {
case AsyncTaskTypeTopologyAnalysis, AsyncTaskTypePerformanceAnalysis,

View File

@ -91,7 +91,10 @@ func NewBusbarSection(name string) (*BusbarSection, error) {
}
func (b *BusbarSection) BusNameLenCheck() bool {
return len([]rune(b.BusbarName)) <= 20
if len([]rune(b.BusbarName)) > 20 {
return false
}
return true
}
func (b *BusbarSection) BusVoltageCheck() bool {
@ -102,5 +105,8 @@ func (b *BusbarSection) BusVoltageCheck() bool {
}
func (b *BusbarSection) BusDescLenCheck() bool {
return len([]rune(b.BusbarDesc)) <= 100
if len([]rune(b.BusbarDesc)) > 100 {
return false
}
return true
}

View File

@ -14,7 +14,6 @@ type Measurement struct {
Name string `gorm:"column:name;size:64;not null;default:''"`
Type int16 `gorm:"column:type;not null;default:-1"`
Size int `gorm:"column:size;not null;default:-1"`
Mode int16 `gorm:"column:mode;not null;default:1"`
DataSource JSONMap `gorm:"column:data_source;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}}'"`

View File

@ -16,4 +16,5 @@ type MeasurementSet struct {
StationToCompNSPaths map[string][]string // Key: StationTag, Value: NSPaths
CompNSPathToCompTags map[string][]string // Key: NSPaths, Value: CompTags
CompTagToMeasTags map[string][]string // Key: CompTag, Value: MeasTags
CompNSPathToMeasTags map[string][]string // Key: NSPaths, Value: MeasTags
}

View File

@ -1,16 +0,0 @@
// 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)
}

View File

@ -27,6 +27,5 @@ func RegisterRoutes(engine *gin.Engine, clientToken string) {
registerDataRoutes(routeGroup)
registerMonitorRoutes(routeGroup)
registerComponentRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
registerDataObjectRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
registerAsyncTaskRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
}

View File

@ -1,82 +0,0 @@
// 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`
)

View File

@ -2,7 +2,7 @@
package sql
// RecursiveSQL define topologic table recursive query statement
const RecursiveSQL = `WITH RECURSIVE recursive_tree as (
var RecursiveSQL = `WITH RECURSIVE recursive_tree as (
SELECT uuid_from,uuid_to,flag
FROM "topologic"
WHERE uuid_from = ?
@ -16,7 +16,7 @@ const RecursiveSQL = `WITH RECURSIVE recursive_tree as (
// RecursiveTopologicByStartSQL returns every directed edge reachable from the
// supplied start component. It tracks the visited node path inside PostgreSQL
// so cycles in topologic data cannot recurse forever.
const RecursiveTopologicByStartSQL = `WITH RECURSIVE recursive_tree as (
var RecursiveTopologicByStartSQL = `WITH RECURSIVE recursive_tree as (
SELECT uuid_from, uuid_to, flag, ARRAY[uuid_from, uuid_to] AS path
FROM "topologic"
WHERE uuid_from = ?

View File

@ -46,10 +46,12 @@ func GetLongestCommonPrefixLength(query string, result string) int {
return 0
}
minLen := min(len(query), len(result))
queryRunes := []rune(query)
resultRunes := []rune(result)
minLen := min(len(queryRunes), len(resultRunes))
for i := range minLen {
if query[i] != result[i] {
if queryRunes[i] != resultRunes[i] {
return i
}
}

View File

@ -6,8 +6,8 @@ import (
"time"
)
// GenNanoTsStr define func to generate nanosecond timestamp string by current time
func GenNanoTsStr() string {
// GenNanoTSStr define func to generate nanosecond timestamp string by current time
func GenNanoTSStr() string {
now := time.Now()
nanoseconds := now.UnixNano()
timestampStr := strconv.FormatInt(nanoseconds, 10)