2026-07-15 17:33:55 +08:00
|
|
|
// Package handler provides HTTP handlers for various endpoints.
|
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"modelRT/common"
|
|
|
|
|
"modelRT/common/errcode"
|
|
|
|
|
"modelRT/constants"
|
|
|
|
|
"modelRT/database"
|
|
|
|
|
"modelRT/diagram"
|
|
|
|
|
"modelRT/logger"
|
|
|
|
|
"modelRT/model"
|
|
|
|
|
"modelRT/orm"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// DataObjectAttributeQueryHandler define data object attribute value query process API
|
|
|
|
|
func DataObjectAttributeQueryHandler(c *gin.Context) {
|
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
|
pgClient := database.GetPostgresDBClient()
|
|
|
|
|
|
|
|
|
|
token := c.Param("token")
|
|
|
|
|
field := c.Param("field")
|
|
|
|
|
if token == "" {
|
|
|
|
|
err := fmt.Errorf("tokens is missing from the path")
|
|
|
|
|
logger.Error(ctx, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
} else if field == "" {
|
|
|
|
|
err := fmt.Errorf("field is missing from the path")
|
|
|
|
|
logger.Error(ctx, "query field from path failed", "error", err, "url", c.Request.RequestURI)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dataObjectType, err := model.ClassifyDataObjectToken(token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error(ctx, "classify data object token failed", "error", err, "url", c.Request.RequestURI)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 16:04:39 +08:00
|
|
|
normalizedField := strings.ToLower(field)
|
|
|
|
|
if err := validateDataObjectField(token, dataObjectType, normalizedField); err != nil {
|
|
|
|
|
logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var parameter *database.ParameterDataObject
|
2026-07-15 17:33:55 +08:00
|
|
|
var measurement *orm.Measurement
|
|
|
|
|
var measurementComponent *orm.Component
|
|
|
|
|
switch dataObjectType {
|
|
|
|
|
case constants.DataObjectTypeParameter:
|
2026-07-16 16:04:39 +08:00
|
|
|
// 参量支持两种形式token4.token5.token6.token7与token1.token2.token3.token4.token5.token6.token7
|
|
|
|
|
parameter, err = database.QueryParameterByDataObjectToken(ctx, pgClient, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, common.ErrInvalidParameterToken) ||
|
|
|
|
|
errors.Is(err, common.ErrParameterTokenNotFound) ||
|
|
|
|
|
errors.Is(err, common.ErrAmbiguousParameterToken) {
|
|
|
|
|
logger.Warn(ctx, "validate parameter token failed", "token", token, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Error(ctx, "query parameter token from postgres failed", "token", token, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeServerError, "validate parameter token failed", nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-15 17:33:55 +08:00
|
|
|
case constants.DataObjectTypeMeasurement:
|
2026-07-16 16:04:39 +08:00
|
|
|
// 量测支持token1.token2.token3.token4.token5.token6.token7、token4.token5.token6.token7、token4.token7
|
2026-07-15 17:33:55 +08:00
|
|
|
measurement, measurementComponent, err = database.QueryMeasurementByDataObjectToken(ctx, pgClient, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, common.ErrInvalidMeasurementToken) ||
|
|
|
|
|
errors.Is(err, common.ErrMeasurementTokenNotFound) ||
|
|
|
|
|
errors.Is(err, common.ErrAmbiguousMeasurementToken) {
|
|
|
|
|
logger.Warn(ctx, "validate measurement token failed", "token", token, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Error(ctx, "query measurement token from postgres failed", "token", token, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeServerError, "validate measurement token failed", nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch dataObjectType {
|
|
|
|
|
case constants.DataObjectTypeParameter:
|
2026-07-16 16:04:39 +08:00
|
|
|
value, err := buildParameterAttributeValue(
|
|
|
|
|
ctx,
|
|
|
|
|
normalizedField,
|
|
|
|
|
parameter,
|
|
|
|
|
func(ctx context.Context, parameter *database.ParameterDataObject) (any, error) {
|
|
|
|
|
return database.QueryParameterDataObjectValue(ctx, pgClient, parameter)
|
|
|
|
|
},
|
|
|
|
|
func(ctx context.Context, attributeName string) (string, error) {
|
|
|
|
|
return database.QueryParameterAttributeDescription(ctx, pgClient, attributeName)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, common.ErrUnsupportedParameterField) {
|
|
|
|
|
logger.Warn(ctx, "query unsupported parameter field", "token", token, "field", field, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Error(ctx, "build parameter attribute value failed", "token", token, "field", field, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeServerError, "query parameter attribute failed", nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := dataObjectAttributeQueryResult{
|
|
|
|
|
Token: token,
|
|
|
|
|
Field: normalizedField,
|
|
|
|
|
Code: errcode.ErrProcessSuccess.Code(),
|
|
|
|
|
Msg: errcode.ErrProcessSuccess.Msg(),
|
|
|
|
|
Value: value,
|
|
|
|
|
}
|
|
|
|
|
renderRespSuccess(c, constants.RespCodeSuccess, "query parameter attribute success", map[string]any{
|
|
|
|
|
"attributes": []dataObjectAttributeQueryResult{result},
|
|
|
|
|
})
|
2026-07-15 17:33:55 +08:00
|
|
|
case constants.DataObjectTypeMeasurement:
|
|
|
|
|
value, err := buildMeasurementAttributeValue(
|
|
|
|
|
ctx,
|
|
|
|
|
normalizedField,
|
|
|
|
|
measurement,
|
|
|
|
|
measurementComponent,
|
|
|
|
|
queryMeasurementRealtimeValue,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, common.ErrUnsupportedMeasurementField) {
|
|
|
|
|
logger.Warn(ctx, "query unsupported measurement field", "token", token, "field", field, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Error(ctx, "build measurement attribute value failed", "token", token, "field", field, "error", err)
|
|
|
|
|
renderRespFailure(c, constants.RespCodeServerError, "query measurement attribute failed", nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := dataObjectAttributeQueryResult{
|
|
|
|
|
Token: token,
|
|
|
|
|
Field: normalizedField,
|
|
|
|
|
Code: errcode.ErrProcessSuccess.Code(),
|
|
|
|
|
Msg: errcode.ErrProcessSuccess.Msg(),
|
|
|
|
|
Value: value,
|
|
|
|
|
}
|
|
|
|
|
renderRespSuccess(c, constants.RespCodeSuccess, "query measurement attribute success", map[string]any{
|
|
|
|
|
"attributes": []dataObjectAttributeQueryResult{result},
|
|
|
|
|
})
|
|
|
|
|
default:
|
|
|
|
|
renderRespFailure(c, constants.RespCodeInvalidParams, "invalid data object type", nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type measurementValueLoader func(context.Context, orm.JSONMap) (any, error)
|
|
|
|
|
|
2026-07-16 16:04:39 +08:00
|
|
|
type parameterValueLoader func(context.Context, *database.ParameterDataObject) (any, error)
|
|
|
|
|
|
|
|
|
|
type parameterDescriptionLoader func(context.Context, string) (string, error)
|
|
|
|
|
|
|
|
|
|
var measurementDataObjectFields = map[string]struct{}{
|
|
|
|
|
"value": {},
|
|
|
|
|
"mode": {},
|
|
|
|
|
"meta": {},
|
|
|
|
|
"type": {},
|
|
|
|
|
"name": {},
|
|
|
|
|
"description": {},
|
|
|
|
|
"id": {},
|
|
|
|
|
"size": {},
|
|
|
|
|
"data_source": {},
|
|
|
|
|
"event_plan": {},
|
|
|
|
|
"binding": {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var parameterDataObjectFields = map[string]struct{}{
|
|
|
|
|
"value": {},
|
|
|
|
|
"name": {},
|
|
|
|
|
"meta": {},
|
|
|
|
|
"type": {},
|
|
|
|
|
"description": {},
|
|
|
|
|
"id": {},
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:33:55 +08:00
|
|
|
type dataObjectAttributeQueryResult struct {
|
|
|
|
|
Token string `json:"token"`
|
|
|
|
|
Field string `json:"field"`
|
|
|
|
|
Code int `json:"code"`
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
Value any `json:"value"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 16:04:39 +08:00
|
|
|
func validateDataObjectField(token string, dataObjectType constants.DataObjectType, field string) error {
|
|
|
|
|
field = strings.ToLower(field)
|
|
|
|
|
switch dataObjectType {
|
|
|
|
|
case constants.DataObjectTypeMeasurement:
|
|
|
|
|
if _, ok := measurementDataObjectFields[field]; ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("%w: %s", common.ErrUnsupportedMeasurementField, field)
|
|
|
|
|
case constants.DataObjectTypeParameter:
|
|
|
|
|
if _, ok := parameterDataObjectFields[field]; ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("%w: %s", common.ErrUnsupportedParameterField, field)
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("invalid data object type %q", dataObjectType)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildParameterAttributeValue(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
field string,
|
|
|
|
|
parameter *database.ParameterDataObject,
|
|
|
|
|
loadValue parameterValueLoader,
|
|
|
|
|
loadDescription parameterDescriptionLoader,
|
|
|
|
|
) (any, error) {
|
|
|
|
|
if parameter == nil {
|
|
|
|
|
return nil, fmt.Errorf("parameter data object is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
component := parameter.Component
|
|
|
|
|
switch field {
|
|
|
|
|
case "value":
|
|
|
|
|
if loadValue == nil {
|
|
|
|
|
return nil, fmt.Errorf("parameter value loader is nil")
|
|
|
|
|
}
|
|
|
|
|
return loadValue(ctx, parameter)
|
|
|
|
|
case "meta":
|
|
|
|
|
return "PARAM", nil
|
|
|
|
|
case "type":
|
|
|
|
|
return parameter.AttributeType, nil
|
|
|
|
|
case "name":
|
|
|
|
|
return strings.Join([]string{
|
|
|
|
|
component.NSPath,
|
|
|
|
|
component.Tag,
|
|
|
|
|
parameter.AttributeGroup,
|
|
|
|
|
parameter.AttributeName,
|
|
|
|
|
}, "."), nil
|
|
|
|
|
case "description":
|
|
|
|
|
if loadDescription == nil {
|
|
|
|
|
return nil, fmt.Errorf("parameter description loader is nil")
|
|
|
|
|
}
|
|
|
|
|
return loadDescription(ctx, parameter.AttributeName)
|
|
|
|
|
case "id":
|
|
|
|
|
return strings.Join([]string{
|
|
|
|
|
component.GridName,
|
|
|
|
|
component.ZoneName,
|
|
|
|
|
component.StationName,
|
|
|
|
|
component.NSPath,
|
|
|
|
|
component.Tag,
|
|
|
|
|
parameter.AttributeGroup,
|
|
|
|
|
parameter.AttributeName,
|
|
|
|
|
}, "."), nil
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("%w: %s", common.ErrUnsupportedParameterField, field)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-15 17:33:55 +08:00
|
|
|
func buildMeasurementAttributeValue(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
field string,
|
|
|
|
|
measurement *orm.Measurement,
|
|
|
|
|
component *orm.Component,
|
|
|
|
|
loadValue measurementValueLoader,
|
|
|
|
|
) (any, error) {
|
|
|
|
|
if measurement == nil {
|
|
|
|
|
return nil, fmt.Errorf("measurement is nil")
|
|
|
|
|
}
|
|
|
|
|
if component == nil {
|
|
|
|
|
return nil, fmt.Errorf("measurement component is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch field {
|
|
|
|
|
case "value":
|
|
|
|
|
if loadValue == nil {
|
|
|
|
|
return nil, fmt.Errorf("measurement value loader is nil")
|
|
|
|
|
}
|
|
|
|
|
return loadValue(ctx, measurement.DataSource)
|
|
|
|
|
case "mode":
|
|
|
|
|
fmt.Println("measurement.Mode:", measurement.Mode)
|
|
|
|
|
switch measurement.Mode {
|
|
|
|
|
case 0:
|
|
|
|
|
return false, nil
|
|
|
|
|
case 1:
|
|
|
|
|
return true, nil
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("invalid measurement mode %d: expected 0 or 1", measurement.Mode)
|
|
|
|
|
}
|
|
|
|
|
case "meta":
|
|
|
|
|
return "MEASUREMENT", nil
|
|
|
|
|
case "type":
|
|
|
|
|
return model.MeasurementTypeFromDataSource(measurement.DataSource)
|
|
|
|
|
case "name":
|
|
|
|
|
// The resolved measurement and component prove that token4.token7 exists.
|
|
|
|
|
return component.NSPath + "." + measurement.Tag, nil
|
|
|
|
|
case "description":
|
|
|
|
|
return measurement.Name, nil
|
|
|
|
|
case "id":
|
|
|
|
|
return strings.Join([]string{
|
|
|
|
|
component.GridName,
|
|
|
|
|
component.ZoneName,
|
|
|
|
|
component.StationName,
|
|
|
|
|
component.NSPath,
|
|
|
|
|
component.Tag,
|
|
|
|
|
"bay",
|
|
|
|
|
measurement.Tag,
|
|
|
|
|
}, "."), nil
|
|
|
|
|
case "size":
|
|
|
|
|
return measurement.Size, nil
|
|
|
|
|
case "data_source":
|
|
|
|
|
return measurement.DataSource, nil
|
|
|
|
|
case "event_plan":
|
|
|
|
|
return measurement.EventPlan, nil
|
|
|
|
|
case "binding":
|
|
|
|
|
return measurement.Binding, nil
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("%w: %s", common.ErrUnsupportedMeasurementField, field)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func queryMeasurementRealtimeValue(ctx context.Context, dataSource orm.JSONMap) (any, error) {
|
|
|
|
|
queryKey, err := model.GenerateMeasureIdentifier(dataSource)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("generate measurement redis key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
value, err := diagram.NewRedisClient().QueryLatestMeasurementValue(ctx, queryKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("query real-time measurement value by key %q: %w", queryKey, err)
|
|
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
}
|