Compare commits

...

13 Commits

Author SHA1 Message Date
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
45 changed files with 2412 additions and 511 deletions

View File

@ -4,6 +4,8 @@ package constants
const ( const (
// DefaultScore define the default score for redissearch suggestion // DefaultScore define the default score for redissearch suggestion
DefaultScore = 1.0 DefaultScore = 1.0
// ComponentConfigKey define component config token used at token6
ComponentConfigKey = "component"
) )
const ( const (
@ -42,6 +44,9 @@ const (
// RedisSpecCompTagMeasSetKey define redis set key which store all measurement tag keys under specific component tag // RedisSpecCompTagMeasSetKey define redis set key which store all measurement tag keys under specific component tag
RedisSpecCompTagMeasSetKey = "%s_measurement_tag_keys" RedisSpecCompTagMeasSetKey = "%s_measurement_tag_keys"
// RedisSpecCompNSPathMeasSetKey define redis set key which store all measurement tag keys under specific component nspath
RedisSpecCompNSPathMeasSetKey = "%s_nspath_measurement_tag_keys"
) )
const ( const (

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

@ -28,6 +28,7 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
StationToCompNSPaths: make(map[string][]string), StationToCompNSPaths: make(map[string][]string),
CompNSPathToCompTags: make(map[string][]string), CompNSPathToCompTags: make(map[string][]string),
CompTagToMeasTags: make(map[string][]string), CompTagToMeasTags: make(map[string][]string),
CompNSPathToMeasTags: make(map[string][]string),
} }
g, gctx := errgroup.WithContext(ctx) g, gctx := errgroup.WithContext(ctx)
@ -114,10 +115,13 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
var measurements []struct { var measurements []struct {
orm.Measurement orm.Measurement
CompTag string `gorm:"column:comp_tag"` CompTag string `gorm:"column:comp_tag"`
CompNSPath string `gorm:"column:comp_nspath"`
BayTag string `gorm:"column:bay_tag"`
} }
if err := db.Table("measurement"). if err := db.Table("measurement").
Select("measurement.*, component.tag as comp_tag"). 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 component on measurement.component_uuid = component.global_uuid").
Joins("left join bay on measurement.bay_uuid = bay.bay_uuid").
Scan(&measurements).Error; err != nil { Scan(&measurements).Error; err != nil {
return fmt.Errorf("query measurements: %w", err) return fmt.Errorf("query measurements: %w", err)
} }
@ -126,6 +130,9 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
if m.CompTag != "" { if m.CompTag != "" {
mSet.CompTagToMeasTags[m.CompTag] = append(mSet.CompTagToMeasTags[m.CompTag], m.Tag) 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 return nil
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,12 +13,13 @@ import (
// AttrDeleteHandler deletes a data attribute // AttrDeleteHandler deletes a data attribute
func AttrDeleteHandler(c *gin.Context) { func AttrDeleteHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrDeleteRequest var request network.AttrDeleteRequest
clientToken := c.GetString("client_token") clientToken := c.GetString("client_token")
if clientToken == "" { if clientToken == "" {
err := common.ErrGetClientToken err := common.ErrGetClientToken
logger.Error(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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -27,7 +28,7 @@ func AttrDeleteHandler(c *gin.Context) {
} }
if err := c.ShouldBindJSON(&request); err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -35,9 +36,9 @@ func AttrDeleteHandler(c *gin.Context) {
return 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 { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),

View File

@ -13,13 +13,14 @@ import (
// AttrGetHandler retrieves the value of a data attribute // AttrGetHandler retrieves the value of a data attribute
func AttrGetHandler(c *gin.Context) { func AttrGetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrGetRequest var request network.AttrGetRequest
clientToken := c.GetString("client_token") clientToken := c.GetString("client_token")
if clientToken == "" { if clientToken == "" {
err := common.ErrGetClientToken err := common.ErrGetClientToken
logger.Error(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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -28,7 +29,7 @@ func AttrGetHandler(c *gin.Context) {
} }
if err := c.ShouldBindJSON(&request); err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -37,12 +38,12 @@ func AttrGetHandler(c *gin.Context) {
} }
pgClient := database.GetPostgresDBClient() 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 { if err != nil {
tx.Rollback() 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),

View File

@ -13,13 +13,14 @@ import (
// AttrSetHandler sets the value of a data attribute // AttrSetHandler sets the value of a data attribute
func AttrSetHandler(c *gin.Context) { func AttrSetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.AttrSetRequest var request network.AttrSetRequest
clientToken := c.GetString("client_token") clientToken := c.GetString("client_token")
if clientToken == "" { if clientToken == "" {
err := common.ErrGetClientToken err := common.ErrGetClientToken
logger.Error(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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -28,7 +29,7 @@ func AttrSetHandler(c *gin.Context) {
} }
if err := c.ShouldBindJSON(&request); err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -37,9 +38,9 @@ func AttrSetHandler(c *gin.Context) {
} }
// The logic for handling Redis operations directly from the handler // The logic for handling Redis operations directly from the handler
rs := diagram.NewRedisString(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 { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),

View File

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

View File

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

View File

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

View File

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

View File

@ -22,12 +22,13 @@ import (
// ComponentAttributeQueryHandler define circuit diagram component attribute value query process API // ComponentAttributeQueryHandler define circuit diagram component attribute value query process API
func ComponentAttributeQueryHandler(c *gin.Context) { func ComponentAttributeQueryHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient() pgClient := database.GetPostgresDBClient()
tokens := c.Param("tokens") tokens := c.Param("tokens")
if tokens == "" { if tokens == "" {
err := fmt.Errorf("tokens is missing from the path") err := fmt.Errorf("tokens is missing from the path")
logger.Error(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) renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return return
} }
@ -54,10 +55,10 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
dbQueryMap := make(map[string][]cacheQueryItem) dbQueryMap := make(map[string][]cacheQueryItem)
var secondaryQueryCount int var secondaryQueryCount int
for hSetKey, items := range cacheQueryMap { for hSetKey, items := range cacheQueryMap {
hset := diagram.NewRedisHash(c, hSetKey, 5000, false) hset := diagram.NewRedisHash(ctx, hSetKey, 5000, false)
cacheData, err := hset.HGetAll() cacheData, err := hset.HGetAll()
if err != nil { 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 { for _, item := range items {
if val, ok := cacheData[item.attributeName]; ok { if val, ok := cacheData[item.attributeName]; ok {
@ -75,9 +76,9 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
return return
} }
tx := pgClient.WithContext(c).Begin() tx := pgClient.WithContext(ctx).Begin()
if tx.Error != nil { 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) fillRemainingErrors(queryResults, tokenSlice, errcode.ErrBeginTxFailed)
payload := genQueryRespPayload(queryResults, tokenSlice) payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "begin postgres database transaction failed", payload) renderRespFailure(c, constants.RespCodeServerError, "begin postgres database transaction failed", payload)
@ -86,9 +87,9 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
defer tx.Rollback() defer tx.Rollback()
allCompTags := slices.Collect(maps.Keys(dbQueryMap)) allCompTags := slices.Collect(maps.Keys(dbQueryMap))
compModelMap, err := database.QueryComponentByCompTags(c, tx, allCompTags) compModelMap, err := database.QueryComponentByCompTags(ctx, tx, allCompTags)
if err != nil { 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) fillRemainingErrors(queryResults, tokenSlice, errcode.ErrDBQueryFailed)
payload := genQueryRespPayload(queryResults, tokenSlice) payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "query component meta failed", payload) renderRespFailure(c, constants.RespCodeServerError, "query component meta failed", payload)
@ -116,7 +117,7 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
tableNameMap, err := database.BatchGetProjectNames(tx, identifiers) tableNameMap, err := database.BatchGetProjectNames(tx, identifiers)
if err != nil { if err != nil {
logger.Error(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) fillRemainingErrors(queryResults, tokenSlice, errcode.ErrRetrieveFailed)
payload := genQueryRespPayload(queryResults, tokenSlice) payload := genQueryRespPayload(queryResults, tokenSlice)
renderRespFailure(c, constants.RespCodeServerError, "batch get table names from postgres database failed", payload) renderRespFailure(c, constants.RespCodeServerError, "batch get table names from postgres database failed", payload)
@ -151,10 +152,11 @@ func ComponentAttributeQueryHandler(c *gin.Context) {
} }
if err := tx.Commit().Error; err != nil { 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 { } else {
backfillCtx := context.WithoutCancel(ctx)
for hKey, items := range redisSyncMap { 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 // ComponentAttributeUpdateHandler define circuit diagram component attribute value update process API
func ComponentAttributeUpdateHandler(c *gin.Context) { func ComponentAttributeUpdateHandler(c *gin.Context) {
ctx := c.Request.Context()
pgClient := database.GetPostgresDBClient() pgClient := database.GetPostgresDBClient()
var request network.ComponentAttributeUpdateInfo var request network.ComponentAttributeUpdateInfo
if err := c.ShouldBindJSON(&request); err != nil { if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "unmarshal request params failed", "error", err) logger.Error(ctx, "unmarshal request params failed", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil) renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return return
} }
@ -54,16 +55,16 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
} }
// open transaction // open transaction
tx := pgClient.WithContext(c).Begin() tx := pgClient.WithContext(ctx).Begin()
if tx.Error != nil { 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) renderRespFailure(c, constants.RespCodeServerError, "begin postgres transaction failed", nil)
return return
} }
compInfo, err := database.QueryComponentByCompTag(c, tx, attributeComponentTag) compInfo, err := database.QueryComponentByCompTag(ctx, tx, attributeComponentTag)
if err != nil { 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 { for _, attribute := range request.AttributeConfigs {
if _, exists := updateResults[attribute.AttributeToken]; !exists { if _, exists := updateResults[attribute.AttributeToken]; !exists {
@ -139,7 +140,7 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
} }
for key, items := range redisUpdateMap { 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)) fields := make(map[string]any, len(items))
for _, item := range items { for _, item := range items {
@ -147,7 +148,7 @@ func ComponentAttributeUpdateHandler(c *gin.Context) {
} }
if err := hset.SetRedisHashByMap(fields); err != nil { 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 { for _, item := range items {
if _, exists := updateResults[item.token]; exists { if _, exists := updateResults[item.token]; exists {

View File

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

View File

@ -16,11 +16,12 @@ import (
// QueryHistoryDataHandler define query history data process API // QueryHistoryDataHandler define query history data process API
func QueryHistoryDataHandler(c *gin.Context) { func QueryHistoryDataHandler(c *gin.Context) {
ctx := c.Request.Context()
token := c.Query("token") token := c.Query("token")
beginStr := c.Query("begin") beginStr := c.Query("begin")
begin, err := strconv.Atoi(beginStr) begin, err := strconv.Atoi(beginStr)
if err != nil { if err != nil {
logger.Error(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{ resp := network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
@ -31,7 +32,7 @@ func QueryHistoryDataHandler(c *gin.Context) {
endStr := c.Query("end") endStr := c.Query("end")
end, err := strconv.Atoi(endStr) end, err := strconv.Atoi(endStr)
if err != nil { if err != nil {
logger.Error(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{ resp := network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,

View File

@ -15,13 +15,14 @@ import (
// MeasurementGetHandler define measurement query API // MeasurementGetHandler define measurement query API
func MeasurementGetHandler(c *gin.Context) { func MeasurementGetHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementGetRequest var request network.MeasurementGetRequest
clientToken := c.GetString("client_token") clientToken := c.GetString("client_token")
if clientToken == "" { if clientToken == "" {
err := common.ErrGetClientToken err := common.ErrGetClientToken
logger.Error(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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -30,7 +31,7 @@ func MeasurementGetHandler(c *gin.Context) {
} }
if err := c.ShouldBindJSON(&request); err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -38,10 +39,10 @@ func MeasurementGetHandler(c *gin.Context) {
return 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) points, err := zset.ZRANGE(request.MeasurementToken, 0, -1)
if err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusInternalServerError, Code: http.StatusInternalServerError,
Msg: err.Error(), Msg: err.Error(),
@ -54,9 +55,9 @@ func MeasurementGetHandler(c *gin.Context) {
} }
pgClient := database.GetPostgresDBClient() pgClient := database.GetPostgresDBClient()
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, request.MeasurementID) measurementInfo, err := database.QueryMeasurementByID(ctx, pgClient, request.MeasurementID)
if err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),

View File

@ -2,11 +2,13 @@
package handler package handler
import ( import (
"errors"
"strings"
"modelRT/constants"
"modelRT/logger" "modelRT/logger"
"modelRT/model" "modelRT/model"
"modelRT/network" "modelRT/network"
"modelRT/util"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -43,79 +45,81 @@ import (
// //
// @Router /measurement/recommend [get] // @Router /measurement/recommend [get]
func MeasurementRecommendHandler(c *gin.Context) { func MeasurementRecommendHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementRecommendRequest var request network.MeasurementRecommendRequest
if err := c.ShouldBindQuery(&request); err != nil { if err := c.ShouldBindQuery(&request); err != nil {
logger.Error(c, "failed to bind measurement recommend request", "error", err) logger.Error(ctx, "failed to bind measurement recommend request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{ renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
Code: http.StatusBadRequest, return
Msg: err.Error(), }
if err := validateMeasurementRecommendInput(request.Input); err != nil {
logger.Warn(ctx, "invalid measurement recommend input", "input", request.Input, "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), map[string]any{
"input": request.Input,
}) })
return return
} }
recommendResults := model.RedisSearchRecommend(ctx, request.Input)
payload := network.MeasurementRecommendPayload{
Input: request.Input,
RecommendedList: make([]string, 0),
}
seen := make(map[string]struct{})
orderedResults := orderedRecommendResults(recommendResults)
recommendResults := model.RedisSearchRecommend(c, request.Input) for _, recommendResult := range orderedResults {
payloads := make([]network.MeasurementRecommendPayload, 0, len(recommendResults))
for _, recommendResult := range recommendResults {
if recommendResult.Err != nil { if recommendResult.Err != nil {
err := recommendResult.Err err := recommendResult.Err
logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err) logger.Error(ctx, "failed to get recommend data from redis", "input", request.Input, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{ renderRespFailure(c, constants.RespCodeServerError, err.Error(), map[string]any{
Code: http.StatusInternalServerError,
Msg: err.Error(),
Payload: map[string]any{
"input": request.Input, "input": request.Input,
},
}) })
return return
} }
var finalOffset int if recommendResult.Offset > payload.Offset {
recommends := recommendResult.QueryDatas payload.Offset = recommendResult.Offset
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
}
resultRecommends := make([]string, 0, len(recommends)) for _, recommendResult := range orderedResults {
seen := make(map[string]struct{}) for _, recommend := range recommendResult.QueryDatas {
if _, exists := seen[recommend]; !exists {
for _, recommend := range recommends { seen[recommend] = struct{}{}
recommendTerm := recommend[finalOffset:] payload.RecommendedList = append(payload.RecommendedList, recommend)
if len(recommendTerm) != 0 {
if _, exists := seen[recommendTerm]; !exists {
seen[recommendTerm] = struct{}{}
resultRecommends = append(resultRecommends, recommendTerm)
} }
} }
} }
payloads = append(payloads, network.MeasurementRecommendPayload{ renderRespSuccess(c, constants.RespCodeSuccess, "success", payload)
Input: request.Input,
Offset: finalOffset,
RecommendType: recommendResult.RecommendType.String(),
RecommendedList: resultRecommends,
})
} }
c.JSON(http.StatusOK, network.SuccessResponse{ func orderedRecommendResults(recommendResults map[string]model.SearchResult) []model.SearchResult {
Code: http.StatusOK, orderedTypes := []string{
Msg: "success", constants.CompNSPathRecommendHierarchyType.String(),
Payload: &payloads, 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 // MeasurementLinkHandler defines the measurement link process api
func MeasurementLinkHandler(c *gin.Context) { func MeasurementLinkHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.MeasurementLinkRequest var request network.MeasurementLinkRequest
clientToken := c.GetString("client_token") clientToken := c.GetString("client_token")
if clientToken == "" { if clientToken == "" {
err := common.ErrGetClientToken err := common.ErrGetClientToken
logger.Error(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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -31,7 +32,7 @@ func MeasurementLinkHandler(c *gin.Context) {
} }
if err := c.ShouldBindJSON(&request); err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
@ -44,9 +45,9 @@ func MeasurementLinkHandler(c *gin.Context) {
pgClient := database.GetPostgresDBClient() pgClient := database.GetPostgresDBClient()
measurementID := request.MeasurementID measurementID := request.MeasurementID
action := request.Action action := request.Action
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, measurementID) measurementInfo, err := database.QueryMeasurementByID(ctx, pgClient, measurementID)
if err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
@ -59,9 +60,9 @@ func MeasurementLinkHandler(c *gin.Context) {
return return
} }
componentInfo, err := database.QueryComponentByUUID(c, pgClient, measurementInfo.ComponentUUID) componentInfo, err := database.QueryComponentByUUID(ctx, pgClient, measurementInfo.ComponentUUID)
if err != nil { 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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
@ -74,9 +75,9 @@ func MeasurementLinkHandler(c *gin.Context) {
return return
} }
allMeasSet := diagram.NewRedisSet(c, constants.RedisAllMeasTagSetKey, 0, false) allMeasSet := diagram.NewRedisSet(ctx, constants.RedisAllMeasTagSetKey, 0, false)
compMeasLinkKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, componentInfo.Tag) compMeasLinkKey := fmt.Sprintf(constants.RedisSpecCompTagMeasSetKey, componentInfo.Tag)
compMeasLinkSet := diagram.NewRedisSet(c, compMeasLinkKey, 0, false) compMeasLinkSet := diagram.NewRedisSet(ctx, compMeasLinkKey, 0, false)
switch action { switch action {
case constants.SearchLinkAddAction: case constants.SearchLinkAddAction:
@ -84,18 +85,18 @@ func MeasurementLinkHandler(c *gin.Context) {
err2 := compMeasLinkSet.SADD(measurementInfo.Tag) err2 := compMeasLinkSet.SADD(measurementInfo.Tag)
err = processActionError(err1, err2, action) err = processActionError(err1, err2, action)
if err != nil { if err != nil {
logger.Error(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: case constants.SearchLinkDelAction:
err1 := allMeasSet.SREM(measurementInfo.Tag) err1 := allMeasSet.SREM(measurementInfo.Tag)
err2 := compMeasLinkSet.SREM(measurementInfo.Tag) err2 := compMeasLinkSet.SREM(measurementInfo.Tag)
err = processActionError(err1, err2, action) err = processActionError(err1, err2, action)
if err != nil { if err != nil {
logger.Error(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: default:
err = common.ErrUnsupportedLinkAction 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 { if err != nil {
@ -110,7 +111,7 @@ func MeasurementLinkHandler(c *gin.Context) {
return 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{ c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK, Code: http.StatusOK,

View File

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

View File

@ -60,10 +60,11 @@ var wsUpgrader = websocket.Upgrader{
// //
// @Router /data/realtime [get] // @Router /data/realtime [get]
func QueryRealTimeDataHandler(c *gin.Context) { func QueryRealTimeDataHandler(c *gin.Context) {
ctx := c.Request.Context()
var request network.RealTimeQueryRequest var request network.RealTimeQueryRequest
if err := c.ShouldBindJSON(&request); err != nil { if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(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{ c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Msg: err.Error(), Msg: err.Error(),
@ -73,7 +74,7 @@ func QueryRealTimeDataHandler(c *gin.Context) {
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
logger.Error(c, "upgrade http protocol to websocket protocal failed", "error", err) logger.Error(ctx, "upgrade http protocol to websocket protocal failed", "error", err)
return return
} }
defer conn.Close() defer conn.Close()
@ -87,29 +88,29 @@ func QueryRealTimeDataHandler(c *gin.Context) {
case data := <-transportChannel: case data := <-transportChannel:
respByte, err := jsoniter.Marshal(data) respByte, err := jsoniter.Marshal(data)
if err != nil { if err != nil {
logger.Error(c, "marshal real time data to bytes failed", "error", err) logger.Error(ctx, "marshal real time data to bytes failed", "error", err)
continue continue
} }
err = conn.WriteMessage(websocket.TextMessage, respByte) err = conn.WriteMessage(websocket.TextMessage, respByte)
if err != nil { if err != nil {
logger.Error(c, "write message to websocket connection failed", "error", err) logger.Error(ctx, "write message to websocket connection failed", "error", err)
continue continue
} }
case <-closeChannel: case <-closeChannel:
logger.Info(c, "data receiving goroutine has been closed") logger.Info(ctx, "data receiving goroutine has been closed")
// TODO 优化时间控制 // TODO 优化时间控制
deadline := time.Now().Add(5 * time.Second) deadline := time.Now().Add(5 * time.Second)
err := conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "the session ended normally"), deadline) err := conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "the session ended normally"), deadline)
if err != nil { if err != nil {
logger.Error(c, "sending close control message failed", "error", err) logger.Error(ctx, "sending close control message failed", "error", err)
} }
// gracefully close session processing // gracefully close session processing
err = conn.Close() err = conn.Close()
if err != nil { if err != nil {
logger.Error(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 // RealTimeDataReceivehandler define real time data receive and process API
func RealTimeDataReceivehandler(c *gin.Context) { func RealTimeDataReceivehandler(c *gin.Context) {
ctx := c.Request.Context()
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
logger.Error(c, "upgrade http protocol to websocket protocal failed", "error", err) logger.Error(ctx, "upgrade http protocol to websocket protocal failed", "error", err)
return return
} }
defer conn.Close() defer conn.Close()
@ -27,17 +28,17 @@ func RealTimeDataReceivehandler(c *gin.Context) {
for { for {
messageType, p, err := conn.ReadMessage() messageType, p, err := conn.ReadMessage()
if err != nil { if err != nil {
logger.Error(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) respByte := processResponse(-1, "read message from websocket connection failed", nil)
if len(respByte) == 0 { 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 continue
} }
err = conn.WriteMessage(messageType, respByte) err = conn.WriteMessage(messageType, respByte)
if err != nil { 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
} }
continue continue
@ -46,17 +47,17 @@ func RealTimeDataReceivehandler(c *gin.Context) {
var request network.RealTimeDataReceiveRequest var request network.RealTimeDataReceiveRequest
err = jsoniter.Unmarshal([]byte(p), &request) err = jsoniter.Unmarshal([]byte(p), &request)
if err != nil { if err != nil {
logger.Error(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) respByte := processResponse(-1, "unmarshal message from byte failed", nil)
if len(respByte) == 0 { 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 continue
} }
err = conn.WriteMessage(messageType, respByte) err = conn.WriteMessage(messageType, respByte)
if err != nil { 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
} }
continue continue
@ -70,13 +71,13 @@ func RealTimeDataReceivehandler(c *gin.Context) {
} }
respByte := processResponse(0, "success", payload) respByte := processResponse(0, "success", payload)
if len(respByte) == 0 { if len(respByte) == 0 {
logger.Error(c, "process message from byte failed", "error", err) logger.Error(ctx, "process message from byte failed", "error", err)
continue continue
} }
err = conn.WriteMessage(messageType, respByte) err = conn.WriteMessage(messageType, respByte)
if err != nil { if err != nil {
logger.Error(c, "write message to websocket connection failed", "error", err) logger.Error(ctx, "write message to websocket connection failed", "error", err)
continue continue
} }
} }

View File

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

View File

@ -18,6 +18,8 @@ type facade struct {
_logger *zap.Logger _logger *zap.Logger
} }
const facadeCallerSkip = 2
// Debug define facade func of debug level log // Debug define facade func of debug level log
func Debug(ctx context.Context, msg string, kv ...any) { func Debug(ctx context.Context, msg string, kv ...any) {
logFacade().log(ctx, zapcore.DebugLevel, msg, kv...) logFacade().log(ctx, zapcore.DebugLevel, msg, kv...)
@ -39,12 +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) { 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) { func (f *facade) logSkip(ctx context.Context, lvl zapcore.Level, extraSkip int, msg string, kv ...any) {
fields := makeLogFieldsSkip(ctx, extraSkip, kv...) fields := makeLogFieldsSkip(ctx, extraSkip, kv...)
ce := f._logger.Check(lvl, msg) logger := f._logger
if extraSkip > 0 {
logger = logger.WithOptions(zap.AddCallerSkip(extraSkip))
}
ce := logger.Check(lvl, msg)
ce.Write(fields...) ce.Write(fields...)
} }
@ -66,7 +72,7 @@ func InfoSkip(ctx context.Context, extraSkip int, msg string, kv ...any) {
func logFacade() *facade { func logFacade() *facade {
fOnce.Do(func() { fOnce.Do(func() {
f = &facade{ f = &facade{
_logger: GetLoggerInstance(), _logger: GetLoggerInstance().WithOptions(zap.AddCallerSkip(facadeCallerSkip)),
} }
}) })
return f return f

12
main.go
View File

@ -235,6 +235,18 @@ func main() {
panic(err) panic(err)
} }
componentColumnNames, err := database.QueryComponentColumnNames(ctx, tx)
if err != nil {
logger.Error(ctx, "query component table column names failed", "error", err)
panic(err)
}
err = model.StoreComponentColumnRecommend(ctx, fullParentPath, isLocalParentPath, componentColumnNames)
if err != nil {
logger.Error(ctx, "store component column recommend content failed", "error", err)
panic(err)
}
allMeasurement, err := database.GetAllMeasurements(ctx, tx) allMeasurement, err := database.GetAllMeasurements(ctx, tx)
if err != nil { if err != nil {
logger.Error(ctx, "load topologic info from postgres failed", "error", err) logger.Error(ctx, "load topologic info from postgres failed", "error", err)

View File

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

@ -90,6 +90,6 @@ func (s *ShortAttrInfo) IsLocal() bool {
} }
// GetAttrValue define return the attribute value // GetAttrValue define return the attribute value
func (l *ShortAttrInfo) GetAttrValue() any { func (s *ShortAttrInfo) GetAttrValue() any {
return l.AttrValue return s.AttrValue
} }

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

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

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,3 +1,4 @@
// Package model define model struct of model runtime service
package model package model
import ( import (
@ -7,11 +8,12 @@ import (
// SelectModelByType define select the data structure for parsing based on the input model type // SelectModelByType define select the data structure for parsing based on the input model type
func SelectModelByType(modelType int) BasicModelInterface { func SelectModelByType(modelType int) BasicModelInterface {
if modelType == constants.BusbarType { switch modelType {
case constants.BusbarType:
return &orm.BusbarSection{} return &orm.BusbarSection{}
} else if modelType == constants.AsyncMotorType { case constants.AsyncMotorType:
return &orm.AsyncMotor{} return &orm.AsyncMotor{}
} else if modelType == constants.DemoType { case constants.DemoType:
return &orm.Demo{} return &orm.Demo{}
} }
return nil return nil

View File

@ -53,6 +53,7 @@ func CleanupRecommendRedisCache(ctx context.Context) error {
"*_component_nspath_keys", // correspond RedisSpecStationCompNSPATHSetKey "*_component_nspath_keys", // correspond RedisSpecStationCompNSPATHSetKey
"*_component_tag_keys", // correspond RedisSpecCompNSPathCompTagSetKey "*_component_tag_keys", // correspond RedisSpecCompNSPathCompTagSetKey
"*_measurement_tag_keys", // correspond RedisSpecCompTagMeasSetKey "*_measurement_tag_keys", // correspond RedisSpecCompTagMeasSetKey
"*_nspath_measurement_tag_keys", // correspond RedisSpecCompNSPathMeasSetKey
} }
for _, pattern := range patterns { for _, pattern := range patterns {

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,854 @@
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 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 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 { type MeasurementRecommendPayload struct {
Input string `json:"input" example:"transformfeeder1_220."` Input string `json:"input" example:"transformfeeder1_220."`
Offset int `json:"offset" example:"21"` Offset int `json:"offset" example:"21"`
RecommendType string `json:"recommended_type" example:"grid_tag"`
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"` RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"`
} }

View File

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

View File

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

View File

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