feat(data-object)!: move recommendation API to data-object routes

- move recommend endpoint to /data-object/recommend
- rename measurement recommendation handlers and DTOs to data-object
- update Swagger documentation
- remove unused token argument from field validation

BREAKING CHANGE:
- /measurement/recommend has been removed
- use /data-object/recommend instead
This commit is contained in:
douxu 2026-07-20 11:32:40 +08:00
parent ab029e292b
commit d26260b2a7
12 changed files with 209 additions and 207 deletions

View File

@ -23,6 +23,57 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/data-object/recommend": {
"get": {
"description": "根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"DataObject Recommend"
],
"summary": "测量点推荐(搜索框自动补全)",
"parameters": [
{
"type": "string",
"example": "\"grid1\"",
"description": "推荐关键词,例如 'grid1' 或 'grid1.'",
"name": "input",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "返回推荐列表成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.DataObjectRecommendPayload"
}
}
}
]
}
},
"400": {
"description": "返回推荐列表失败",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/data/realtime": { "/data/realtime": {
"get": { "get": {
"description": "根据用户输入的组件token,从 dataRT 服务中持续获取测点实时数据", "description": "根据用户输入的组件token,从 dataRT 服务中持续获取测点实时数据",
@ -87,57 +138,6 @@ const docTemplate = `{
} }
} }
}, },
"/measurement/recommend": {
"get": {
"description": "根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Measurement Recommend"
],
"summary": "测量点推荐(搜索框自动补全)",
"parameters": [
{
"type": "string",
"example": "\"grid1\"",
"description": "推荐关键词,例如 'grid1' 或 'grid1.'",
"name": "input",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "返回推荐列表成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.MeasurementRecommendPayload"
}
}
}
]
}
},
"400": {
"description": "返回推荐列表失败",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/model/diagram_load/{page_id}": { "/model/diagram_load/{page_id}": {
"get": { "get": {
"description": "load circuit diagram info by page id", "description": "load circuit diagram info by page id",
@ -487,23 +487,7 @@ const docTemplate = `{
} }
} }
}, },
"network.FailureResponse": { "network.DataObjectRecommendPayload": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 3000
},
"msg": {
"type": "string",
"example": "process completed with partial failures"
},
"payload": {
"type": "object"
}
}
},
"network.MeasurementRecommendPayload": {
"type": "object", "type": "object",
"properties": { "properties": {
"input": { "input": {
@ -527,6 +511,22 @@ const docTemplate = `{
} }
} }
}, },
"network.FailureResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 3000
},
"msg": {
"type": "string",
"example": "process completed with partial failures"
},
"payload": {
"type": "object"
}
}
},
"network.RealTimeDataPayload": { "network.RealTimeDataPayload": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -17,6 +17,57 @@
"host": "localhost:8080", "host": "localhost:8080",
"basePath": "/api/v1", "basePath": "/api/v1",
"paths": { "paths": {
"/data-object/recommend": {
"get": {
"description": "根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"DataObject Recommend"
],
"summary": "测量点推荐(搜索框自动补全)",
"parameters": [
{
"type": "string",
"example": "\"grid1\"",
"description": "推荐关键词,例如 'grid1' 或 'grid1.'",
"name": "input",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "返回推荐列表成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.DataObjectRecommendPayload"
}
}
}
]
}
},
"400": {
"description": "返回推荐列表失败",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/data/realtime": { "/data/realtime": {
"get": { "get": {
"description": "根据用户输入的组件token,从 dataRT 服务中持续获取测点实时数据", "description": "根据用户输入的组件token,从 dataRT 服务中持续获取测点实时数据",
@ -81,57 +132,6 @@
} }
} }
}, },
"/measurement/recommend": {
"get": {
"description": "根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Measurement Recommend"
],
"summary": "测量点推荐(搜索框自动补全)",
"parameters": [
{
"type": "string",
"example": "\"grid1\"",
"description": "推荐关键词,例如 'grid1' 或 'grid1.'",
"name": "input",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "返回推荐列表成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.MeasurementRecommendPayload"
}
}
}
]
}
},
"400": {
"description": "返回推荐列表失败",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/model/diagram_load/{page_id}": { "/model/diagram_load/{page_id}": {
"get": { "get": {
"description": "load circuit diagram info by page id", "description": "load circuit diagram info by page id",
@ -481,23 +481,7 @@
} }
} }
}, },
"network.FailureResponse": { "network.DataObjectRecommendPayload": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 3000
},
"msg": {
"type": "string",
"example": "process completed with partial failures"
},
"payload": {
"type": "object"
}
}
},
"network.MeasurementRecommendPayload": {
"type": "object", "type": "object",
"properties": { "properties": {
"input": { "input": {
@ -521,6 +505,22 @@
} }
} }
}, },
"network.FailureResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 3000
},
"msg": {
"type": "string",
"example": "process completed with partial failures"
},
"payload": {
"type": "object"
}
}
},
"network.RealTimeDataPayload": { "network.RealTimeDataPayload": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -59,18 +59,7 @@ definitions:
example: 3 example: 3
type: integer type: integer
type: object type: object
network.FailureResponse: network.DataObjectRecommendPayload:
properties:
code:
example: 3000
type: integer
msg:
example: process completed with partial failures
type: string
payload:
type: object
type: object
network.MeasurementRecommendPayload:
properties: properties:
input: input:
example: transformfeeder1_220. example: transformfeeder1_220.
@ -87,6 +76,17 @@ definitions:
type: string type: string
type: array type: array
type: object type: object
network.FailureResponse:
properties:
code:
example: 3000
type: integer
msg:
example: process completed with partial failures
type: string
payload:
type: object
type: object
network.RealTimeDataPayload: network.RealTimeDataPayload:
properties: properties:
sub_pos: sub_pos:
@ -169,6 +169,37 @@ info:
title: ModelRT 实时模型服务 API 文档 title: ModelRT 实时模型服务 API 文档
version: "1.0" version: "1.0"
paths: paths:
/data-object/recommend:
get:
consumes:
- application/json
description: 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。
parameters:
- description: 推荐关键词,例如 'grid1' 或 'grid1.'
example: '"grid1"'
in: query
name: input
required: true
type: string
produces:
- application/json
responses:
"200":
description: 返回推荐列表成功
schema:
allOf:
- $ref: '#/definitions/network.SuccessResponse'
- properties:
payload:
$ref: '#/definitions/network.DataObjectRecommendPayload'
type: object
"400":
description: 返回推荐列表失败
schema:
$ref: '#/definitions/network.FailureResponse'
summary: 测量点推荐(搜索框自动补全)
tags:
- DataObject Recommend
/data/realtime: /data/realtime:
get: get:
consumes: consumes:
@ -209,37 +240,6 @@ paths:
summary: 获取实时测点数据 summary: 获取实时测点数据
tags: tags:
- RealTime Component - RealTime Component
/measurement/recommend:
get:
consumes:
- application/json
description: 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。
parameters:
- description: 推荐关键词,例如 'grid1' 或 'grid1.'
example: '"grid1"'
in: query
name: input
required: true
type: string
produces:
- application/json
responses:
"200":
description: 返回推荐列表成功
schema:
allOf:
- $ref: '#/definitions/network.SuccessResponse'
- properties:
payload:
$ref: '#/definitions/network.MeasurementRecommendPayload'
type: object
"400":
description: 返回推荐列表失败
schema:
$ref: '#/definitions/network.FailureResponse'
summary: 测量点推荐(搜索框自动补全)
tags:
- Measurement Recommend
/model/diagram_load/{page_id}: /model/diagram_load/{page_id}:
get: get:
consumes: consumes:

View File

@ -38,7 +38,7 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
return return
} }
if err := validateDataObjectField(token, dataObjectType, field); err != nil { if err := validateDataObjectField(dataObjectType, field); err != nil {
logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err) logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil) renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return return
@ -202,7 +202,7 @@ type dataObjectAttributeQueryResult struct {
Value any `json:"value"` Value any `json:"value"`
} }
func validateDataObjectField(token string, dataObjectType constants.DataObjectType, field string) error { func validateDataObjectField(dataObjectType constants.DataObjectType, field string) error {
field = strings.ToLower(field) field = strings.ToLower(field)
switch dataObjectType { switch dataObjectType {
case constants.DataObjectTypeMeasurement: case constants.DataObjectTypeMeasurement:

View File

@ -104,7 +104,7 @@ func TestValidateDataObjectField(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := validateDataObjectField(tt.token, tt.dataObjectType, tt.field) err := validateDataObjectField(tt.dataObjectType, tt.field)
if tt.wantErr == nil { if tt.wantErr == nil {
require.NoError(t, err) require.NoError(t, err)
return return

View File

@ -13,14 +13,14 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// MeasurementRecommendHandler define measurement recommend API // DataObjectRecommendHandler define data-object recommend API
// @Summary 测量点推荐(搜索框自动补全) // @Summary 测量点推荐(搜索框自动补全)
// @Description 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。 // @Description 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。
// @Tags Measurement Recommend // @Tags DataObject Recommend
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param input query string true "推荐关键词,例如 'grid1' 或 'grid1.'" Example("grid1") // @Param input query string true "推荐关键词,例如 'grid1' 或 'grid1.'" Example("grid1")
// @Success 200 {object} network.SuccessResponse{payload=network.MeasurementRecommendPayload} "返回推荐列表成功" // @Success 200 {object} network.SuccessResponse{payload=network.DataObjectRecommendPayload} "返回推荐列表成功"
// //
// @Example 200 { // @Example 200 {
// "code": 200, // "code": 200,
@ -43,25 +43,25 @@ import (
// "msg": "failed to get recommend data from redis", // "msg": "failed to get recommend data from redis",
// } // }
// //
// @Router /measurement/recommend [get] // @Router /data-object/recommend [get]
func MeasurementRecommendHandler(c *gin.Context) { func DataObjectRecommendHandler(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
var request network.MeasurementRecommendRequest var request network.DataObjectRecommendRequest
if err := c.ShouldBindQuery(&request); err != nil { if err := c.ShouldBindQuery(&request); err != nil {
logger.Error(ctx, "failed to bind measurement recommend request", "error", err) logger.Error(ctx, "failed to bind data object recommend request", "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil) renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
return return
} }
if err := validateMeasurementRecommendInput(request.Input); err != nil { if err := validateDataObjectRecommendInput(request.Input); err != nil {
logger.Warn(ctx, "invalid measurement recommend input", "input", request.Input, "error", err) logger.Warn(ctx, "invalid data object recommend input", "input", request.Input, "error", err)
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), map[string]any{ renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), map[string]any{
"input": request.Input, "input": request.Input,
}) })
return return
} }
recommendResults := model.RedisSearchRecommend(ctx, request.Input) recommendResults := model.RedisSearchRecommend(ctx, request.Input)
payload := network.MeasurementRecommendPayload{ payload := network.DataObjectRecommendPayload{
Input: request.Input, Input: request.Input,
RecommendedList: make([]string, 0), RecommendedList: make([]string, 0),
} }
@ -117,7 +117,7 @@ func orderedRecommendResults(recommendResults map[string]model.SearchResult) []m
return results return results
} }
func validateMeasurementRecommendInput(input string) error { func validateDataObjectRecommendInput(input string) error {
if strings.Contains(input, "..") { if strings.Contains(input, "..") {
return errors.New("input contains continuous dots") return errors.New("input contains continuous dots")
} }

View File

@ -32,7 +32,7 @@ func TestValidateMeasurementRecommendInput(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
err := validateMeasurementRecommendInput(tt.input) err := validateDataObjectRecommendInput(tt.input)
if tt.valid && err != nil { if tt.valid && err != nil {
t.Fatalf("expected valid input, got error %v", err) t.Fatalf("expected valid input, got error %v", err)
} }

View File

@ -0,0 +1,7 @@
// Package network define struct of network operation
package network
// DataObjectRecommendRequest defines the request payload for an data object recommend
type DataObjectRecommendRequest struct {
Input string `form:"input,omitempty" example:"grid1"`
}

View File

@ -6,8 +6,3 @@ type MeasurementGetRequest struct {
MeasurementID int64 `json:"measurement_id" example:"1001"` MeasurementID int64 `json:"measurement_id" example:"1001"`
MeasurementToken string `json:"token" example:"some-token"` MeasurementToken string `json:"token" example:"some-token"`
} }
// MeasurementRecommendRequest defines the request payload for an measurement recommend
type MeasurementRecommendRequest struct {
Input string `form:"input,omitempty" example:"grid1"`
}

View File

@ -22,13 +22,6 @@ type WSResponse struct {
Payload any `json:"payload,omitempty" swaggertype:"object"` Payload any `json:"payload,omitempty" swaggertype:"object"`
} }
// MeasurementRecommendPayload define struct of represents the data payload for the successful recommendation response.
type MeasurementRecommendPayload struct {
Input string `json:"input" example:"transformfeeder1_220."`
Offset int `json:"offset" example:"21"`
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"`
}
// TargetResult define struct of target item in real time data subscription response payload // TargetResult define struct of target item in real time data subscription response payload
type TargetResult struct { type TargetResult struct {
ID string `json:"id" example:"grid1.zone1.station1.ns1.tag1.transformfeeder1_220.I_A_rms"` ID string `json:"id" example:"grid1.zone1.station1.ns1.tag1.transformfeeder1_220.I_A_rms"`
@ -41,3 +34,10 @@ type RealTimeSubPayload struct {
ClientID string `json:"client_id" example:"5d72f2d9-e33a-4f1b-9c76-88a44b9a953e" description:"用于标识不同client的监控请求ID"` ClientID string `json:"client_id" example:"5d72f2d9-e33a-4f1b-9c76-88a44b9a953e" description:"用于标识不同client的监控请求ID"`
TargetResults []TargetResult `json:"targets"` TargetResults []TargetResult `json:"targets"`
} }
// DataObjectRecommendPayload define struct of represents the data payload for the successful recommendation response
type DataObjectRecommendPayload struct {
Input string `json:"input" example:"transformfeeder1_220."`
Offset int `json:"offset" example:"21"`
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"`
}

View File

@ -13,4 +13,5 @@ func registerDataObjectRoutes(rg *gin.RouterGroup, middlewares ...gin.HandlerFun
g.Use(middlewares...) g.Use(middlewares...)
g.GET("attribute", handler.DataObjectAttributeQueryHandler) g.GET("attribute", handler.DataObjectAttributeQueryHandler)
g.PATCH("attribute", handler.DataObjectAttributeUpdateHandler) g.PATCH("attribute", handler.DataObjectAttributeUpdateHandler)
g.GET("recommend", handler.DataObjectRecommendHandler)
} }

View File

@ -12,5 +12,4 @@ func registerMeasurementRoutes(rg *gin.RouterGroup, middlewares ...gin.HandlerFu
g := rg.Group("/measurement/") g := rg.Group("/measurement/")
g.Use(middlewares...) g.Use(middlewares...)
g.GET("load", handler.MeasurementGetHandler) g.GET("load", handler.MeasurementGetHandler)
g.GET("recommend", handler.MeasurementRecommendHandler)
} }