feat: implement topology analysis async task with BFS connectivity check

- add TopologyAnalysisHandler.Execute() with 5-phase BFS reachability
    check between start/end component UUIDs; support CheckInService flag
    to skip out-of-service nodes during traversal
  - carry task params through RabbitMQ message (TaskQueueMessage.Params)
    instead of re-querying DB in handler; update TaskHandler.Execute
    interface and all handler signatures accordingly
  - fix BuildMultiBranchTree UUIDFrom condition bug; return nodeMap for
    O(1) lookup; add QueryTopologicByStartUUID for directed traversal
  - add QueryBayByUUID/QueryBaysByUUIDs and
    QueryComponentsInServiceByUUIDs (two-column select) to database layer
  - add diagram.FindPath via LCA algorithm for tree path reconstruction
  - move initTracerProvider to middleware.InitTracerProvider; add
    OtelConfig struct to ModelRTConfig for endpoint configuration
  - update topology analysis params to start/end_component_uuid +
    check_in_service; remove dead topology init code
This commit is contained in:
douxu 2026-04-24 17:14:46 +08:00
parent 03bd058558
commit 1b1f43db7f
41 changed files with 1962 additions and 283 deletions

View File

@ -92,6 +92,12 @@ type DataRTConfig struct {
Method string `mapstructure:"polling_api_method"` Method string `mapstructure:"polling_api_method"`
} }
// OtelConfig define config struct of OpenTelemetry tracing
type OtelConfig struct {
Endpoint string `mapstructure:"endpoint"` // e.g. "localhost:4318"
Insecure bool `mapstructure:"insecure"`
}
// AsyncTaskConfig define config struct of asynchronous task system // AsyncTaskConfig define config struct of asynchronous task system
type AsyncTaskConfig struct { type AsyncTaskConfig struct {
WorkerPoolSize int `mapstructure:"worker_pool_size"` WorkerPoolSize int `mapstructure:"worker_pool_size"`
@ -115,6 +121,7 @@ type ModelRTConfig struct {
LockerRedisConfig RedisConfig `mapstructure:"locker_redis"` LockerRedisConfig RedisConfig `mapstructure:"locker_redis"`
StorageRedisConfig RedisConfig `mapstructure:"storage_redis"` StorageRedisConfig RedisConfig `mapstructure:"storage_redis"`
AsyncTaskConfig AsyncTaskConfig `mapstructure:"async_task"` AsyncTaskConfig AsyncTaskConfig `mapstructure:"async_task"`
OtelConfig OtelConfig `mapstructure:"otel"`
PostgresDBURI string `mapstructure:"-"` PostgresDBURI string `mapstructure:"-"`
} }

View File

@ -33,7 +33,7 @@ func CreateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo netwo
Name: componentInfo.Name, Name: componentInfo.Name,
Context: componentInfo.Context, Context: componentInfo.Context,
Op: componentInfo.Op, Op: componentInfo.Op,
Ts: time.Now(), TS: time.Now(),
} }
result := tx.WithContext(cancelCtx).Create(&component) result := tx.WithContext(cancelCtx).Create(&component)

View File

@ -35,7 +35,7 @@ func CreateMeasurement(ctx context.Context, tx *gorm.DB, measurementInfo network
BayUUID: globalUUID, BayUUID: globalUUID,
ComponentUUID: globalUUID, ComponentUUID: globalUUID,
Op: -1, Op: -1,
Ts: time.Now(), TS: time.Now(),
} }
result := tx.WithContext(cancelCtx).Create(&measurement) result := tx.WithContext(cancelCtx).Create(&measurement)

56
database/query_bay.go Normal file
View File

@ -0,0 +1,56 @@
// Package database define database operation functions
package database
import (
"context"
"time"
"modelRT/logger"
"modelRT/orm"
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// QueryBayByUUID returns the Bay record matching bayUUID.
func QueryBayByUUID(ctx context.Context, tx *gorm.DB, bayUUID uuid.UUID) (*orm.Bay, error) {
var bay orm.Bay
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).
Where("bay_uuid = ?", bayUUID).
Clauses(clause.Locking{Strength: "UPDATE"}).
First(&bay)
if result.Error != nil {
return nil, result.Error
}
return &bay, nil
}
// QueryBaysByUUIDs returns Bay records matching the given UUIDs in a single query.
// The returned slice preserves database order; unmatched UUIDs are silently omitted.
func QueryBaysByUUIDs(ctx context.Context, tx *gorm.DB, bayUUIDs []uuid.UUID) ([]orm.Bay, error) {
if len(bayUUIDs) == 0 {
return nil, nil
}
var bays []orm.Bay
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).
Where("bay_uuid IN ?", bayUUIDs).
Clauses(clause.Locking{Strength: "UPDATE"}).
Find(&bays)
if result.Error != nil {
logger.Error(ctx, "query bays by uuids failed", "error", result.Error)
return nil, result.Error
}
return bays, nil
}

View File

@ -148,6 +148,39 @@ func QueryLongIdentModelInfoByToken(ctx context.Context, tx *gorm.DB, measTag st
return &resultComp, &meauserment, nil return &resultComp, &meauserment, nil
} }
// QueryComponentsInServiceByUUIDs returns a map of global_uuid → in_service for the
// given UUIDs. Only global_uuid and in_service columns are selected for efficiency.
func QueryComponentsInServiceByUUIDs(ctx context.Context, tx *gorm.DB, uuids []uuid.UUID) (map[uuid.UUID]bool, error) {
if len(uuids) == 0 {
return make(map[uuid.UUID]bool), nil
}
type row struct {
GlobalUUID uuid.UUID `gorm:"column:global_uuid"`
InService bool `gorm:"column:in_service"`
}
var rows []row
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).
Model(&orm.Component{}).
Select("global_uuid, in_service").
Where("global_uuid IN ?", uuids).
Scan(&rows)
if result.Error != nil {
return nil, result.Error
}
m := make(map[uuid.UUID]bool, len(rows))
for _, r := range rows {
m[r.GlobalUUID] = r.InService
}
return m, nil
}
// QueryShortIdentModelInfoByToken define func to query short identity model info by short token // QueryShortIdentModelInfoByToken define func to query short identity model info by short token
func QueryShortIdentModelInfoByToken(ctx context.Context, tx *gorm.DB, measTag string, condition *orm.Component) (*orm.Component, *orm.Measurement, error) { func QueryShortIdentModelInfoByToken(ctx context.Context, tx *gorm.DB, measTag string, condition *orm.Component) (*orm.Component, *orm.Measurement, error) {
var resultComp orm.Component var resultComp orm.Component

View File

@ -32,71 +32,51 @@ func QueryTopologic(ctx context.Context, tx *gorm.DB) ([]orm.Topologic, error) {
return topologics, nil return topologics, nil
} }
// QueryTopologicFromDB return the result of query topologic info from DB // QueryTopologicByStartUUID returns all edges reachable from startUUID following
func QueryTopologicFromDB(ctx context.Context, tx *gorm.DB) (*diagram.MultiBranchTreeNode, error) { // directed uuid_from → uuid_to edges in the topologic table.
func QueryTopologicByStartUUID(ctx context.Context, tx *gorm.DB, startUUID uuid.UUID) ([]orm.Topologic, error) {
var topologics []orm.Topologic
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Raw(sql.RecursiveSQL, startUUID).
Scan(&topologics)
if result.Error != nil {
logger.Error(ctx, "query topologic by start uuid failed", "start_uuid", startUUID, "error", result.Error)
return nil, result.Error
}
return topologics, nil
}
// QueryTopologicFromDB return the result of query topologic info from DB.
// Returns the root node and a flat nodeMap for O(1) lookup by UUID.
func QueryTopologicFromDB(ctx context.Context, tx *gorm.DB) (*diagram.MultiBranchTreeNode, map[uuid.UUID]*diagram.MultiBranchTreeNode, error) {
topologicInfos, err := QueryTopologic(ctx, tx) topologicInfos, err := QueryTopologic(ctx, tx)
if err != nil { if err != nil {
logger.Error(ctx, "query topologic info failed", "error", err) logger.Error(ctx, "query topologic info failed", "error", err)
return nil, err return nil, nil, err
} }
tree, err := BuildMultiBranchTree(topologicInfos) tree, nodeMap, err := BuildMultiBranchTree(topologicInfos)
if err != nil { if err != nil {
logger.Error(ctx, "init topologic failed", "error", err) logger.Error(ctx, "init topologic failed", "error", err)
return nil, err return nil, nil, err
} }
return tree, nil return tree, nodeMap, nil
} }
// InitCircuitDiagramTopologic return circuit diagram topologic info from postgres // BuildMultiBranchTree return the multi branch tree by topologic info.
func InitCircuitDiagramTopologic(topologicNodes []orm.Topologic) error { // Returns the root node and a flat nodeMap for O(1) lookup by UUID.
var rootVertex *diagram.MultiBranchTreeNode func BuildMultiBranchTree(topologics []orm.Topologic) (*diagram.MultiBranchTreeNode, map[uuid.UUID]*diagram.MultiBranchTreeNode, error) {
for _, node := range topologicNodes {
if node.UUIDFrom == constants.UUIDNil {
rootVertex = diagram.NewMultiBranchTree(node.UUIDFrom)
break
}
}
if rootVertex == nil {
return fmt.Errorf("root vertex is nil")
}
for _, node := range topologicNodes {
if node.UUIDFrom == constants.UUIDNil {
nodeVertex := diagram.NewMultiBranchTree(node.UUIDTo)
rootVertex.AddChild(nodeVertex)
}
}
node := rootVertex
for _, nodeVertex := range node.Children {
nextVertexs := make([]*diagram.MultiBranchTreeNode, 0)
nextVertexs = append(nextVertexs, nodeVertex)
}
return nil
}
// TODO 电流互感器不单独划分间隔,以母线、浇筑母线、变压器为间隔原件
func IntervalBoundaryDetermine(uuid uuid.UUID) bool {
diagram.GetComponentMap(uuid.String())
// TODO 判断 component 的类型是否为间隔
// TODO 0xA1B2C3D4,高四位表示可以成为间隔的compoent类型的值为FFFF,普通 component 类型的值为 0000。低四位中前二位表示component的一级类型例如母线 PT、母联/母分、进线等,低四位中后二位表示一级类型中包含的具体类型,例如母线 PT中包含的电压互感器、隔离开关、接地开关、避雷器、带电显示器等。
num := uint32(0xA1B2C3D4) // 八位16进制数
high16 := uint16(num >> 16)
fmt.Printf("原始值: 0x%X\n", num) // 输出: 0xA1B2C3D4
fmt.Printf("高十六位: 0x%X\n", high16) // 输出: 0xA1B2
return true
}
// BuildMultiBranchTree return the multi branch tree by topologic info and component type map
func BuildMultiBranchTree(topologics []orm.Topologic) (*diagram.MultiBranchTreeNode, error) {
nodeMap := make(map[uuid.UUID]*diagram.MultiBranchTreeNode, len(topologics)*2) nodeMap := make(map[uuid.UUID]*diagram.MultiBranchTreeNode, len(topologics)*2)
for _, topo := range topologics { for _, topo := range topologics {
if _, exists := nodeMap[topo.UUIDFrom]; !exists { if _, exists := nodeMap[topo.UUIDFrom]; !exists {
// skip special uuid // UUIDNil is the virtual root sentinel — skip creating a regular node for it
if topo.UUIDTo != constants.UUIDNil { if topo.UUIDFrom != constants.UUIDNil {
nodeMap[topo.UUIDFrom] = &diagram.MultiBranchTreeNode{ nodeMap[topo.UUIDFrom] = &diagram.MultiBranchTreeNode{
ID: topo.UUIDFrom, ID: topo.UUIDFrom,
Children: make([]*diagram.MultiBranchTreeNode, 0), Children: make([]*diagram.MultiBranchTreeNode, 0),
@ -105,7 +85,6 @@ func BuildMultiBranchTree(topologics []orm.Topologic) (*diagram.MultiBranchTreeN
} }
if _, exists := nodeMap[topo.UUIDTo]; !exists { if _, exists := nodeMap[topo.UUIDTo]; !exists {
// skip special uuid
if topo.UUIDTo != constants.UUIDNil { if topo.UUIDTo != constants.UUIDNil {
nodeMap[topo.UUIDTo] = &diagram.MultiBranchTreeNode{ nodeMap[topo.UUIDTo] = &diagram.MultiBranchTreeNode{
ID: topo.UUIDTo, ID: topo.UUIDTo,
@ -118,10 +97,13 @@ func BuildMultiBranchTree(topologics []orm.Topologic) (*diagram.MultiBranchTreeN
for _, topo := range topologics { for _, topo := range topologics {
var parent *diagram.MultiBranchTreeNode var parent *diagram.MultiBranchTreeNode
if topo.UUIDFrom == constants.UUIDNil { if topo.UUIDFrom == constants.UUIDNil {
parent = &diagram.MultiBranchTreeNode{ if _, exists := nodeMap[constants.UUIDNil]; !exists {
nodeMap[constants.UUIDNil] = &diagram.MultiBranchTreeNode{
ID: constants.UUIDNil, ID: constants.UUIDNil,
Children: make([]*diagram.MultiBranchTreeNode, 0),
} }
nodeMap[constants.UUIDNil] = parent }
parent = nodeMap[constants.UUIDNil]
} else { } else {
parent = nodeMap[topo.UUIDFrom] parent = nodeMap[topo.UUIDFrom]
} }
@ -141,7 +123,7 @@ func BuildMultiBranchTree(topologics []orm.Topologic) (*diagram.MultiBranchTreeN
// return root vertex // return root vertex
root, exists := nodeMap[constants.UUIDNil] root, exists := nodeMap[constants.UUIDNil]
if !exists { if !exists {
return nil, fmt.Errorf("root node not found") return nil, nil, fmt.Errorf("root node not found")
} }
return root, nil return root, nodeMap, nil
} }

View File

@ -43,7 +43,7 @@ func UpdateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo netwo
Name: componentInfo.Name, Name: componentInfo.Name,
Context: componentInfo.Context, Context: componentInfo.Context,
Op: componentInfo.Op, Op: componentInfo.Op,
Ts: time.Now(), TS: time.Now(),
} }
result = tx.Model(&orm.Component{}).WithContext(cancelCtx).Where("GLOBAL_UUID = ?", component.GlobalUUID).Updates(&updateParams) result = tx.Model(&orm.Component{}).WithContext(cancelCtx).Where("GLOBAL_UUID = ?", component.GlobalUUID).Updates(&updateParams)

60
deploy/jaeger.yaml Normal file
View File

@ -0,0 +1,60 @@
apiVersion: v1
kind: Service
metadata:
name: jaeger
labels:
app: jaeger
spec:
ports:
- name: ui
port: 16686
targetPort: 16686
nodePort: 31686 # Jaeger UI浏览器访问 http://<NodeIP>:31686
- name: collector-http
port: 14268
targetPort: 14268
nodePort: 31268 # Jaeger 原生 HTTP collector非 OTel
- name: otlp-http
port: 4318
targetPort: 4318
nodePort: 31318 # OTLP HTTP集群外使用 <NodeIP>:31318
- name: otlp-grpc
port: 4317
targetPort: 4317
nodePort: 31317 # OTLP gRPC集群外使用 <NodeIP>:31317
selector:
app: jaeger
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger
spec:
replicas: 1
selector:
matchLabels:
app: jaeger
template:
metadata:
labels:
app: jaeger
spec:
containers:
- name: jaeger
image: jaegertracing/all-in-one:1.56
env:
- name: COLLECTOR_OTLP_ENABLED
value: "true"
ports:
- containerPort: 16686 # UI
- containerPort: 14268 # Jaeger Collector
- containerPort: 4317 # OTLP gRPC
- containerPort: 4318 # OTLP HTTP
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi

View File

@ -1,3 +1,4 @@
// Package diagram provide diagram data structure and operation
package diagram package diagram
import ( import (
@ -31,11 +32,9 @@ func UpdateAnchorValue(componentUUID string, anchorValue string) bool {
// StoreAnchorValue define func of store anchor value with componentUUID and anchor name // StoreAnchorValue define func of store anchor value with componentUUID and anchor name
func StoreAnchorValue(componentUUID string, anchorValue string) { func StoreAnchorValue(componentUUID string, anchorValue string) {
anchorValueOverview.Store(componentUUID, anchorValue) anchorValueOverview.Store(componentUUID, anchorValue)
return
} }
// DeleteAnchorValue define func of delete anchor value with componentUUID // DeleteAnchorValue define func of delete anchor value with componentUUID
func DeleteAnchorValue(componentUUID string) { func DeleteAnchorValue(componentUUID string) {
anchorValueOverview.Delete(componentUUID) anchorValueOverview.Delete(componentUUID)
return
} }

View File

@ -1,3 +1,4 @@
// Package diagram provide diagram data structure and operation
package diagram package diagram
import ( import (
@ -33,11 +34,9 @@ func UpdateComponentMap(componentID int64, componentInfo *orm.Component) bool {
// StoreComponentMap define func of store circuit diagram data with component uuid and component info // StoreComponentMap define func of store circuit diagram data with component uuid and component info
func StoreComponentMap(componentUUID string, componentInfo *orm.Component) { func StoreComponentMap(componentUUID string, componentInfo *orm.Component) {
diagramsOverview.Store(componentUUID, componentInfo) diagramsOverview.Store(componentUUID, componentInfo)
return
} }
// DeleteComponentMap define func of delete circuit diagram data with component uuid // DeleteComponentMap define func of delete circuit diagram data with component uuid
func DeleteComponentMap(componentUUID string) { func DeleteComponentMap(componentUUID string) {
diagramsOverview.Delete(componentUUID) diagramsOverview.Delete(componentUUID)
return
} }

View File

@ -1,3 +1,4 @@
// Package diagram provide diagram data structure and operation
package diagram package diagram
import ( import (
@ -29,5 +30,4 @@ func TestHMSet(t *testing.T) {
fmt.Printf("err:%v\n", err) fmt.Printf("err:%v\n", err)
} }
fmt.Printf("res:%v\n", res) fmt.Printf("res:%v\n", res)
return
} }

View File

@ -1,3 +1,4 @@
// Package diagram provide diagram data structure and operation
package diagram package diagram
import ( import (
@ -62,3 +63,63 @@ func (n *MultiBranchTreeNode) PrintTree(level int) {
child.PrintTree(level + 1) child.PrintTree(level + 1)
} }
} }
// FindPath returns the ordered node sequence from startID to endID using the
// supplied nodeMap for O(1) lookup. It walks each node up to the root to find
// the LCA, then stitches the two half-paths together.
// Returns nil when either node is absent from nodeMap or no path exists.
func FindPath(startID, endID uuid.UUID, nodeMap map[uuid.UUID]*MultiBranchTreeNode) []*MultiBranchTreeNode {
startNode, ok := nodeMap[startID]
if !ok {
return nil
}
endNode, ok := nodeMap[endID]
if !ok {
return nil
}
// collect ancestors (inclusive) from a node up to the root sentinel
ancestors := func(n *MultiBranchTreeNode) []*MultiBranchTreeNode {
var chain []*MultiBranchTreeNode
for n != nil {
chain = append(chain, n)
n = n.Parent
}
return chain
}
startChain := ancestors(startNode) // [start, ..., root]
endChain := ancestors(endNode) // [end, ..., root]
// index startChain by ID for fast LCA detection
startIdx := make(map[uuid.UUID]int, len(startChain))
for i, node := range startChain {
startIdx[node.ID] = i
}
// find LCA: first node in endChain that also appears in startChain
lcaEndPos := -1
lcaStartPos := -1
for i, node := range endChain {
if j, found := startIdx[node.ID]; found {
lcaEndPos = i
lcaStartPos = j
break
}
}
if lcaEndPos < 0 {
return nil // disconnected
}
// path = startChain[0..lcaStartPos] reversed + endChain[lcaEndPos..0] reversed
path := make([]*MultiBranchTreeNode, 0, lcaStartPos+lcaEndPos+1)
for i := 0; i <= lcaStartPos; i++ {
path = append(path, startChain[i])
}
// append end-side (skip LCA to avoid duplication), reversed
for i := lcaEndPos - 1; i >= 0; i-- {
path = append(path, endChain[i])
}
return path
}

View File

@ -1,3 +1,4 @@
// Package diagram provide diagram data structure and operation
package diagram package diagram
import ( import (
@ -39,11 +40,9 @@ func UpdateGrapMap(pageID int64, graphInfo *Graph) bool {
// StoreGraphMap define func of store circuit diagram topologic data with pageID and topologic info // StoreGraphMap define func of store circuit diagram topologic data with pageID and topologic info
func StoreGraphMap(pageID int64, graphInfo *Graph) { func StoreGraphMap(pageID int64, graphInfo *Graph) {
graphOverview.Store(pageID, graphInfo) graphOverview.Store(pageID, graphInfo)
return
} }
// DeleteGraphMap define func of delete circuit diagram topologic data with pageID // DeleteGraphMap define func of delete circuit diagram topologic data with pageID
func DeleteGraphMap(pageID int64) { func DeleteGraphMap(pageID int64) {
graphOverview.Delete(pageID) graphOverview.Delete(pageID)
return
} }

View File

@ -102,13 +102,12 @@ const docTemplate = `{
"summary": "测量点推荐(搜索框自动补全)", "summary": "测量点推荐(搜索框自动补全)",
"parameters": [ "parameters": [
{ {
"description": "查询输入参数,例如 'trans' 或 'transformfeeder1_220.'", "type": "string",
"name": "request", "example": "\"grid1\"",
"in": "body", "description": "推荐关键词,例如 'grid1' 或 'grid1.'",
"required": true, "name": "input",
"schema": { "in": "query",
"$ref": "#/definitions/network.MeasurementRecommendRequest" "required": true
}
} }
], ],
"responses": { "responses": {
@ -176,19 +175,400 @@ const docTemplate = `{
} }
} }
} }
},
"/monitors/data/realtime/stream/:clientID": {
"get": {
"description": "根据用户输入的clientID拉取对应的实时数据",
"tags": [
"RealTime Component Websocket"
],
"summary": "实时数据拉取 websocket api",
"responses": {}
}
},
"/monitors/data/subscriptions": {
"post": {
"description": "根据用户输入的组件token,从 modelRT 服务中开始或结束对于量测节点的实时数据的订阅",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"RealTime Component"
],
"summary": "开始或结束订阅实时数据",
"parameters": [
{
"description": "量测节点实时数据订阅",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/network.RealTimeSubRequest"
}
}
],
"responses": {
"2000": {
"description": "订阅实时数据结果列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.RealTimeSubPayload"
}
}
}
]
}
},
"3000": {
"description": "订阅实时数据结果列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.FailureResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.RealTimeSubPayload"
}
}
}
]
}
}
}
}
},
"/task/async": {
"post": {
"description": "创建新的异步任务并返回任务ID任务将被提交到队列等待处理",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AsyncTask"
],
"summary": "创建异步任务",
"parameters": [
{
"description": "任务创建请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/network.AsyncTaskCreateRequest"
}
}
],
"responses": {
"200": {
"description": "任务创建成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.AsyncTaskCreateResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/task/async/results": {
"get": {
"description": "根据任务ID列表查询异步任务的状态和结果",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AsyncTask"
],
"summary": "查询异步任务结果",
"parameters": [
{
"type": "string",
"description": "任务ID列表用逗号分隔",
"name": "task_ids",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "查询成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.AsyncTaskResultQueryResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/task/async/{task_id}": {
"get": {
"description": "根据任务ID查询异步任务的详细状态和结果",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AsyncTask"
],
"summary": "查询异步任务详情",
"parameters": [
{
"type": "string",
"description": "任务ID",
"name": "task_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "查询成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.AsyncTaskResult"
}
}
}
]
}
},
"400": {
"description": "请求参数错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"404": {
"description": "任务不存在",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/task/async/{task_id}/cancel": {
"post": {
"description": "取消指定ID的异步任务如果任务尚未开始执行",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AsyncTask"
],
"summary": "取消异步任务",
"parameters": [
{
"type": "string",
"description": "任务ID",
"name": "task_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "任务取消成功",
"schema": {
"$ref": "#/definitions/network.SuccessResponse"
}
},
"400": {
"description": "请求参数错误或任务无法取消",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"404": {
"description": "任务不存在",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
"network.AsyncTaskCreateRequest": {
"type": "object",
"properties": {
"params": {
"description": "required: true",
"type": "object"
},
"task_type": {
"description": "required: true\nenum: TOPOLOGY_ANALYSIS, PERFORMANCE_ANALYSIS, EVENT_ANALYSIS, BATCH_IMPORT",
"type": "string",
"example": "TOPOLOGY_ANALYSIS"
}
}
},
"network.AsyncTaskCreateResponse": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"example": "123e4567-e89b-12d3-a456-426614174000"
}
}
},
"network.AsyncTaskResult": {
"type": "object",
"properties": {
"created_at": {
"type": "integer",
"example": 1741846200
},
"error_code": {
"type": "integer",
"example": 400102
},
"error_detail": {
"type": "object"
},
"error_message": {
"type": "string",
"example": "Component UUID not found"
},
"finished_at": {
"type": "integer",
"example": 1741846205
},
"progress": {
"type": "integer",
"example": 65
},
"result": {
"type": "object"
},
"status": {
"type": "string",
"example": "COMPLETED"
},
"task_id": {
"type": "string",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"task_type": {
"type": "string",
"example": "TOPOLOGY_ANALYSIS"
}
}
},
"network.AsyncTaskResultQueryResponse": {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"$ref": "#/definitions/network.AsyncTaskResult"
}
},
"total": {
"type": "integer",
"example": 3
}
}
},
"network.FailureResponse": { "network.FailureResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"code": { "code": {
"type": "integer", "type": "integer",
"example": 500 "example": 3000
}, },
"msg": { "msg": {
"type": "string", "type": "string",
"example": "failed to get recommend data from redis" "example": "process completed with partial failures"
}, },
"payload": { "payload": {
"type": "object" "type": "object"
@ -216,15 +596,10 @@ const docTemplate = `{
" \"I_B_rms\"", " \"I_B_rms\"",
"\"I_C_rms\"]" "\"I_C_rms\"]"
] ]
}
}
}, },
"network.MeasurementRecommendRequest": { "recommended_type": {
"type": "object",
"properties": {
"input": {
"type": "string", "type": "string",
"example": "trans" "example": "grid_tag"
} }
} }
}, },
@ -237,21 +612,93 @@ const docTemplate = `{
} }
} }
}, },
"network.RealTimeMeasurementItem": {
"type": "object",
"properties": {
"interval": {
"type": "string",
"example": "1"
},
"targets": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"[\"grid1.zone1.station1.ns1.tag1.bay.I11_A_rms\"",
"\"grid1.zone1.station1.ns1.tag1.tag1.bay.I11_B_rms\"]"
]
}
}
},
"network.RealTimeSubPayload": {
"type": "object",
"properties": {
"client_id": {
"type": "string",
"example": "5d72f2d9-e33a-4f1b-9c76-88a44b9a953e"
},
"targets": {
"type": "array",
"items": {
"$ref": "#/definitions/network.TargetResult"
}
}
}
},
"network.RealTimeSubRequest": {
"type": "object",
"properties": {
"action": {
"description": "required: true\nenum: [start, stop]",
"type": "string",
"example": "start"
},
"client_id": {
"type": "string",
"example": "5d72f2d9-e33a-4f1b-9c76-88a44b9a953e"
},
"measurements": {
"description": "required: true",
"type": "array",
"items": {
"$ref": "#/definitions/network.RealTimeMeasurementItem"
}
}
}
},
"network.SuccessResponse": { "network.SuccessResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"code": { "code": {
"type": "integer", "type": "integer",
"example": 200 "example": 2000
}, },
"msg": { "msg": {
"type": "string", "type": "string",
"example": "success" "example": "process completed"
}, },
"payload": { "payload": {
"type": "object" "type": "object"
} }
} }
},
"network.TargetResult": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 20000
},
"id": {
"type": "string",
"example": "grid1.zone1.station1.ns1.tag1.transformfeeder1_220.I_A_rms"
},
"msg": {
"type": "string",
"example": "subscription success"
}
}
} }
} }
}` }`

View File

@ -96,13 +96,12 @@
"summary": "测量点推荐(搜索框自动补全)", "summary": "测量点推荐(搜索框自动补全)",
"parameters": [ "parameters": [
{ {
"description": "查询输入参数,例如 'trans' 或 'transformfeeder1_220.'", "type": "string",
"name": "request", "example": "\"grid1\"",
"in": "body", "description": "推荐关键词,例如 'grid1' 或 'grid1.'",
"required": true, "name": "input",
"schema": { "in": "query",
"$ref": "#/definitions/network.MeasurementRecommendRequest" "required": true
}
} }
], ],
"responses": { "responses": {
@ -170,19 +169,400 @@
} }
} }
} }
},
"/monitors/data/realtime/stream/:clientID": {
"get": {
"description": "根据用户输入的clientID拉取对应的实时数据",
"tags": [
"RealTime Component Websocket"
],
"summary": "实时数据拉取 websocket api",
"responses": {}
}
},
"/monitors/data/subscriptions": {
"post": {
"description": "根据用户输入的组件token,从 modelRT 服务中开始或结束对于量测节点的实时数据的订阅",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"RealTime Component"
],
"summary": "开始或结束订阅实时数据",
"parameters": [
{
"description": "量测节点实时数据订阅",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/network.RealTimeSubRequest"
}
}
],
"responses": {
"2000": {
"description": "订阅实时数据结果列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.RealTimeSubPayload"
}
}
}
]
}
},
"3000": {
"description": "订阅实时数据结果列表",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.FailureResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.RealTimeSubPayload"
}
}
}
]
}
}
}
}
},
"/task/async": {
"post": {
"description": "创建新的异步任务并返回任务ID任务将被提交到队列等待处理",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AsyncTask"
],
"summary": "创建异步任务",
"parameters": [
{
"description": "任务创建请求",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/network.AsyncTaskCreateRequest"
}
}
],
"responses": {
"200": {
"description": "任务创建成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.AsyncTaskCreateResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/task/async/results": {
"get": {
"description": "根据任务ID列表查询异步任务的状态和结果",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AsyncTask"
],
"summary": "查询异步任务结果",
"parameters": [
{
"type": "string",
"description": "任务ID列表用逗号分隔",
"name": "task_ids",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "查询成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.AsyncTaskResultQueryResponse"
}
}
}
]
}
},
"400": {
"description": "请求参数错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/task/async/{task_id}": {
"get": {
"description": "根据任务ID查询异步任务的详细状态和结果",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AsyncTask"
],
"summary": "查询异步任务详情",
"parameters": [
{
"type": "string",
"description": "任务ID",
"name": "task_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "查询成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.AsyncTaskResult"
}
}
}
]
}
},
"400": {
"description": "请求参数错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"404": {
"description": "任务不存在",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/task/async/{task_id}/cancel": {
"post": {
"description": "取消指定ID的异步任务如果任务尚未开始执行",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"AsyncTask"
],
"summary": "取消异步任务",
"parameters": [
{
"type": "string",
"description": "任务ID",
"name": "task_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "任务取消成功",
"schema": {
"$ref": "#/definitions/network.SuccessResponse"
}
},
"400": {
"description": "请求参数错误或任务无法取消",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"404": {
"description": "任务不存在",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
},
"500": {
"description": "服务器内部错误",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
"network.AsyncTaskCreateRequest": {
"type": "object",
"properties": {
"params": {
"description": "required: true",
"type": "object"
},
"task_type": {
"description": "required: true\nenum: TOPOLOGY_ANALYSIS, PERFORMANCE_ANALYSIS, EVENT_ANALYSIS, BATCH_IMPORT",
"type": "string",
"example": "TOPOLOGY_ANALYSIS"
}
}
},
"network.AsyncTaskCreateResponse": {
"type": "object",
"properties": {
"task_id": {
"type": "string",
"example": "123e4567-e89b-12d3-a456-426614174000"
}
}
},
"network.AsyncTaskResult": {
"type": "object",
"properties": {
"created_at": {
"type": "integer",
"example": 1741846200
},
"error_code": {
"type": "integer",
"example": 400102
},
"error_detail": {
"type": "object"
},
"error_message": {
"type": "string",
"example": "Component UUID not found"
},
"finished_at": {
"type": "integer",
"example": 1741846205
},
"progress": {
"type": "integer",
"example": 65
},
"result": {
"type": "object"
},
"status": {
"type": "string",
"example": "COMPLETED"
},
"task_id": {
"type": "string",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"task_type": {
"type": "string",
"example": "TOPOLOGY_ANALYSIS"
}
}
},
"network.AsyncTaskResultQueryResponse": {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"$ref": "#/definitions/network.AsyncTaskResult"
}
},
"total": {
"type": "integer",
"example": 3
}
}
},
"network.FailureResponse": { "network.FailureResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"code": { "code": {
"type": "integer", "type": "integer",
"example": 500 "example": 3000
}, },
"msg": { "msg": {
"type": "string", "type": "string",
"example": "failed to get recommend data from redis" "example": "process completed with partial failures"
}, },
"payload": { "payload": {
"type": "object" "type": "object"
@ -210,15 +590,10 @@
" \"I_B_rms\"", " \"I_B_rms\"",
"\"I_C_rms\"]" "\"I_C_rms\"]"
] ]
}
}
}, },
"network.MeasurementRecommendRequest": { "recommended_type": {
"type": "object",
"properties": {
"input": {
"type": "string", "type": "string",
"example": "trans" "example": "grid_tag"
} }
} }
}, },
@ -231,21 +606,93 @@
} }
} }
}, },
"network.RealTimeMeasurementItem": {
"type": "object",
"properties": {
"interval": {
"type": "string",
"example": "1"
},
"targets": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"[\"grid1.zone1.station1.ns1.tag1.bay.I11_A_rms\"",
"\"grid1.zone1.station1.ns1.tag1.tag1.bay.I11_B_rms\"]"
]
}
}
},
"network.RealTimeSubPayload": {
"type": "object",
"properties": {
"client_id": {
"type": "string",
"example": "5d72f2d9-e33a-4f1b-9c76-88a44b9a953e"
},
"targets": {
"type": "array",
"items": {
"$ref": "#/definitions/network.TargetResult"
}
}
}
},
"network.RealTimeSubRequest": {
"type": "object",
"properties": {
"action": {
"description": "required: true\nenum: [start, stop]",
"type": "string",
"example": "start"
},
"client_id": {
"type": "string",
"example": "5d72f2d9-e33a-4f1b-9c76-88a44b9a953e"
},
"measurements": {
"description": "required: true",
"type": "array",
"items": {
"$ref": "#/definitions/network.RealTimeMeasurementItem"
}
}
}
},
"network.SuccessResponse": { "network.SuccessResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
"code": { "code": {
"type": "integer", "type": "integer",
"example": 200 "example": 2000
}, },
"msg": { "msg": {
"type": "string", "type": "string",
"example": "success" "example": "process completed"
}, },
"payload": { "payload": {
"type": "object" "type": "object"
} }
} }
},
"network.TargetResult": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"example": 20000
},
"id": {
"type": "string",
"example": "grid1.zone1.station1.ns1.tag1.transformfeeder1_220.I_A_rms"
},
"msg": {
"type": "string",
"example": "subscription success"
}
}
} }
} }
} }

View File

@ -1,12 +1,71 @@
basePath: /api/v1 basePath: /api/v1
definitions: definitions:
network.AsyncTaskCreateRequest:
properties:
params:
description: 'required: true'
type: object
task_type:
description: |-
required: true
enum: TOPOLOGY_ANALYSIS, PERFORMANCE_ANALYSIS, EVENT_ANALYSIS, BATCH_IMPORT
example: TOPOLOGY_ANALYSIS
type: string
type: object
network.AsyncTaskCreateResponse:
properties:
task_id:
example: 123e4567-e89b-12d3-a456-426614174000
type: string
type: object
network.AsyncTaskResult:
properties:
created_at:
example: 1741846200
type: integer
error_code:
example: 400102
type: integer
error_detail:
type: object
error_message:
example: Component UUID not found
type: string
finished_at:
example: 1741846205
type: integer
progress:
example: 65
type: integer
result:
type: object
status:
example: COMPLETED
type: string
task_id:
example: 123e4567-e89b-12d3-a456-426614174000
type: string
task_type:
example: TOPOLOGY_ANALYSIS
type: string
type: object
network.AsyncTaskResultQueryResponse:
properties:
tasks:
items:
$ref: '#/definitions/network.AsyncTaskResult'
type: array
total:
example: 3
type: integer
type: object
network.FailureResponse: network.FailureResponse:
properties: properties:
code: code:
example: 500 example: 3000
type: integer type: integer
msg: msg:
example: failed to get recommend data from redis example: process completed with partial failures
type: string type: string
payload: payload:
type: object type: object
@ -27,11 +86,8 @@ definitions:
items: items:
type: string type: string
type: array type: array
type: object recommended_type:
network.MeasurementRecommendRequest: example: grid_tag
properties:
input:
example: trans
type: string type: string
type: object type: object
network.RealTimeDataPayload: network.RealTimeDataPayload:
@ -40,17 +96,69 @@ definitions:
description: TODO 增加example tag description: TODO 增加example tag
type: object type: object
type: object type: object
network.RealTimeMeasurementItem:
properties:
interval:
example: "1"
type: string
targets:
example:
- '["grid1.zone1.station1.ns1.tag1.bay.I11_A_rms"'
- '"grid1.zone1.station1.ns1.tag1.tag1.bay.I11_B_rms"]'
items:
type: string
type: array
type: object
network.RealTimeSubPayload:
properties:
client_id:
example: 5d72f2d9-e33a-4f1b-9c76-88a44b9a953e
type: string
targets:
items:
$ref: '#/definitions/network.TargetResult'
type: array
type: object
network.RealTimeSubRequest:
properties:
action:
description: |-
required: true
enum: [start, stop]
example: start
type: string
client_id:
example: 5d72f2d9-e33a-4f1b-9c76-88a44b9a953e
type: string
measurements:
description: 'required: true'
items:
$ref: '#/definitions/network.RealTimeMeasurementItem'
type: array
type: object
network.SuccessResponse: network.SuccessResponse:
properties: properties:
code: code:
example: 200 example: 2000
type: integer type: integer
msg: msg:
example: success example: process completed
type: string type: string
payload: payload:
type: object type: object
type: object type: object
network.TargetResult:
properties:
code:
example: 20000
type: integer
id:
example: grid1.zone1.station1.ns1.tag1.transformfeeder1_220.I_A_rms
type: string
msg:
example: subscription success
type: string
type: object
host: localhost:8080 host: localhost:8080
info: info:
contact: contact:
@ -110,12 +218,12 @@ paths:
- application/json - application/json
description: 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。 description: 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。
parameters: parameters:
- description: 查询输入参数,例如 'trans' 或 'transformfeeder1_220.' - description: 推荐关键词,例如 'grid1' 或 'grid1.'
in: body example: '"grid1"'
name: request in: query
name: input
required: true required: true
schema: type: string
$ref: '#/definitions/network.MeasurementRecommendRequest'
produces: produces:
- application/json - application/json
responses: responses:
@ -160,4 +268,187 @@ paths:
summary: load circuit diagram info summary: load circuit diagram info
tags: tags:
- load circuit_diagram - load circuit_diagram
/monitors/data/realtime/stream/:clientID:
get:
description: 根据用户输入的clientID拉取对应的实时数据
responses: {}
summary: 实时数据拉取 websocket api
tags:
- RealTime Component Websocket
/monitors/data/subscriptions:
post:
consumes:
- application/json
description: 根据用户输入的组件token,从 modelRT 服务中开始或结束对于量测节点的实时数据的订阅
parameters:
- description: 量测节点实时数据订阅
in: body
name: request
required: true
schema:
$ref: '#/definitions/network.RealTimeSubRequest'
produces:
- application/json
responses:
"2000":
description: 订阅实时数据结果列表
schema:
allOf:
- $ref: '#/definitions/network.SuccessResponse'
- properties:
payload:
$ref: '#/definitions/network.RealTimeSubPayload'
type: object
"3000":
description: 订阅实时数据结果列表
schema:
allOf:
- $ref: '#/definitions/network.FailureResponse'
- properties:
payload:
$ref: '#/definitions/network.RealTimeSubPayload'
type: object
summary: 开始或结束订阅实时数据
tags:
- RealTime Component
/task/async:
post:
consumes:
- application/json
description: 创建新的异步任务并返回任务ID任务将被提交到队列等待处理
parameters:
- description: 任务创建请求
in: body
name: request
required: true
schema:
$ref: '#/definitions/network.AsyncTaskCreateRequest'
produces:
- application/json
responses:
"200":
description: 任务创建成功
schema:
allOf:
- $ref: '#/definitions/network.SuccessResponse'
- properties:
payload:
$ref: '#/definitions/network.AsyncTaskCreateResponse'
type: object
"400":
description: 请求参数错误
schema:
$ref: '#/definitions/network.FailureResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/network.FailureResponse'
summary: 创建异步任务
tags:
- AsyncTask
/task/async/{task_id}:
get:
consumes:
- application/json
description: 根据任务ID查询异步任务的详细状态和结果
parameters:
- description: 任务ID
in: path
name: task_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: 查询成功
schema:
allOf:
- $ref: '#/definitions/network.SuccessResponse'
- properties:
payload:
$ref: '#/definitions/network.AsyncTaskResult'
type: object
"400":
description: 请求参数错误
schema:
$ref: '#/definitions/network.FailureResponse'
"404":
description: 任务不存在
schema:
$ref: '#/definitions/network.FailureResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/network.FailureResponse'
summary: 查询异步任务详情
tags:
- AsyncTask
/task/async/{task_id}/cancel:
post:
consumes:
- application/json
description: 取消指定ID的异步任务如果任务尚未开始执行
parameters:
- description: 任务ID
in: path
name: task_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: 任务取消成功
schema:
$ref: '#/definitions/network.SuccessResponse'
"400":
description: 请求参数错误或任务无法取消
schema:
$ref: '#/definitions/network.FailureResponse'
"404":
description: 任务不存在
schema:
$ref: '#/definitions/network.FailureResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/network.FailureResponse'
summary: 取消异步任务
tags:
- AsyncTask
/task/async/results:
get:
consumes:
- application/json
description: 根据任务ID列表查询异步任务的状态和结果
parameters:
- description: 任务ID列表用逗号分隔
in: query
name: task_ids
required: true
type: string
produces:
- application/json
responses:
"200":
description: 查询成功
schema:
allOf:
- $ref: '#/definitions/network.SuccessResponse'
- properties:
payload:
$ref: '#/definitions/network.AsyncTaskResultQueryResponse'
type: object
"400":
description: 请求参数错误
schema:
$ref: '#/definitions/network.FailureResponse'
"500":
description: 服务器内部错误
schema:
$ref: '#/definitions/network.FailureResponse'
summary: 查询异步任务结果
tags:
- AsyncTask
swagger: "2.0" swagger: "2.0"

37
go.mod
View File

@ -1,6 +1,6 @@
module modelRT module modelRT
go 1.24 go 1.25.0
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
@ -17,11 +17,16 @@ require (
github.com/rabbitmq/amqp091-go v1.10.0 github.com/rabbitmq/amqp091-go v1.10.0
github.com/redis/go-redis/v9 v9.7.3 github.com/redis/go-redis/v9 v9.7.3
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
go.opentelemetry.io/contrib/propagators/b3 v1.43.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9 gorm.io/driver/postgres v1.5.9
@ -33,13 +38,16 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect
@ -49,6 +57,8 @@ require (
github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@ -76,16 +86,23 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.18.0 // indirect golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect golang.org/x/crypto v0.49.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.33.0 // indirect golang.org/x/tools v0.42.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

85
go.sum
View File

@ -17,8 +17,10 @@ github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
@ -42,6 +44,11 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@ -64,13 +71,19 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -126,8 +139,8 @@ github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzuk
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
@ -151,8 +164,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
@ -168,6 +181,26 @@ github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2W
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
@ -178,24 +211,24 @@ golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -203,8 +236,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -212,16 +245,24 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -10,6 +10,8 @@ import (
"modelRT/task" "modelRT/task"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
) )
// AsyncTaskCreateHandler handles creation of asynchronous tasks // AsyncTaskCreateHandler handles creation of asynchronous tasks
@ -67,13 +69,11 @@ func AsyncTaskCreateHandler(c *gin.Context) {
// enqueue task to channel for async publishing to RabbitMQ // enqueue task to channel for async publishing to RabbitMQ
msg := task.NewTaskQueueMessageWithPriority(asyncTask.TaskID, task.TaskType(request.TaskType), 5) msg := task.NewTaskQueueMessageWithPriority(asyncTask.TaskID, task.TaskType(request.TaskType), 5)
// propagate HTTP request trace so the async chain stays on the same traceID // propagate the current OTel span context so the async chain stays on the same trace
if v, _ := ctx.Value(constants.CtxKeyTraceID).(string); v != "" { carrier := make(map[string]string)
msg.TraceID = v otel.GetTextMapPropagator().Inject(ctx, propagation.MapCarrier(carrier))
} msg.TraceCarrier = carrier
if v, _ := ctx.Value(constants.CtxKeySpanID).(string); v != "" { msg.Params = request.Params
msg.SpanID = v
}
task.TaskMsgChan <- msg task.TaskMsgChan <- msg
logger.Info(ctx, "task enqueued to channel", "task_id", asyncTask.TaskID, "queue", constants.TaskQueueName) logger.Info(ctx, "task enqueued to channel", "task_id", asyncTask.TaskID, "queue", constants.TaskQueueName)
@ -102,13 +102,18 @@ func validateTaskParams(taskType string, params map[string]any) bool {
} }
func validateTopologyAnalysisParams(params map[string]any) bool { func validateTopologyAnalysisParams(params map[string]any) bool {
// Check required parameters for topology analysis if v, ok := params["start_component_uuid"]; !ok || v == "" {
if startUUID, ok := params["start_uuid"]; !ok || startUUID == "" {
return false return false
} }
if endUUID, ok := params["end_uuid"]; !ok || endUUID == "" { if v, ok := params["end_component_uuid"]; !ok || v == "" {
return false return false
} }
// check_in_service is optional; validate type when present
if v, exists := params["check_in_service"]; exists {
if _, isBool := v.(bool); !isBool {
return false
}
}
return true return true
} }

View File

@ -6,8 +6,7 @@ import (
"path" "path"
"runtime" "runtime"
"modelRT/constants" "go.opentelemetry.io/otel/trace"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
@ -22,9 +21,6 @@ type Logger interface {
type logger struct { type logger struct {
ctx context.Context ctx context.Context
traceID string
spanID string
pSpanID string
_logger *zap.Logger _logger *zap.Logger
} }
@ -56,10 +52,10 @@ func makeLogFields(ctx context.Context, kv ...any) []zap.Field {
kv = append(kv, "unknown") kv = append(kv, "unknown")
} }
traceID, _ := ctx.Value(constants.CtxKeyTraceID).(string) spanCtx := trace.SpanFromContext(ctx).SpanContext()
spanID, _ := ctx.Value(constants.CtxKeySpanID).(string) traceID := spanCtx.TraceID().String()
parentSpanID, _ := ctx.Value(constants.CtxKeyParentSpanID).(string) spanID := spanCtx.SpanID().String()
kv = append(kv, "traceID", traceID, "spanID", spanID, "parentSpanID", parentSpanID) kv = append(kv, "traceID", traceID, "spanID", spanID)
funcName, file, line := getLoggerCallerInfo() funcName, file, line := getLoggerCallerInfo()
kv = append(kv, "func", funcName, "file", file, "line", line) kv = append(kv, "func", funcName, "file", file, "line", line)
@ -100,25 +96,11 @@ func getLoggerCallerInfo() (funcName, file string, line int) {
return return
} }
// New returns a logger bound to ctx. Trace fields (traceID, spanID, parentSpanID) // New returns a logger bound to ctx. Trace fields (traceID, spanID) are extracted
// are extracted from ctx using typed keys, and are included in every log entry. // from the OTel span stored in ctx and included in every log entry.
func New(ctx context.Context) Logger { func New(ctx context.Context) Logger {
var traceID, spanID, pSpanID string
if v, _ := ctx.Value(constants.CtxKeyTraceID).(string); v != "" {
traceID = v
}
if v, _ := ctx.Value(constants.CtxKeySpanID).(string); v != "" {
spanID = v
}
if v, _ := ctx.Value(constants.CtxKeyParentSpanID).(string); v != "" {
pSpanID = v
}
return &logger{ return &logger{
ctx: ctx, ctx: ctx,
traceID: traceID,
spanID: spanID,
pSpanID: pSpanID,
_logger: GetLoggerInstance(), _logger: GetLoggerInstance(),
} }
} }

24
main.go
View File

@ -19,6 +19,7 @@ import (
"modelRT/database" "modelRT/database"
"modelRT/diagram" "modelRT/diagram"
"modelRT/logger" "modelRT/logger"
"modelRT/middleware"
"modelRT/model" "modelRT/model"
"modelRT/mq" "modelRT/mq"
"modelRT/pool" "modelRT/pool"
@ -38,6 +39,7 @@ import (
"github.com/panjf2000/ants/v2" "github.com/panjf2000/ants/v2"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
"go.opentelemetry.io/otel"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -73,9 +75,6 @@ var (
func main() { func main() {
flag.Parse() flag.Parse()
startupSpanID := util.GenerateSpanID("startup")
ctx := context.WithValue(context.Background(), constants.CtxKeyTraceID, startupSpanID)
ctx = context.WithValue(ctx, constants.CtxKeySpanID, startupSpanID)
configPath := filepath.Join(*modelRTConfigDir, *modelRTConfigName+"."+*modelRTConfigType) configPath := filepath.Join(*modelRTConfigDir, *modelRTConfigName+"."+*modelRTConfigType)
if _, err := os.Stat(configPath); os.IsNotExist(err) { if _, err := os.Stat(configPath); os.IsNotExist(err) {
@ -101,6 +100,22 @@ func main() {
logger.InitLoggerInstance(modelRTConfig.LoggerConfig) logger.InitLoggerInstance(modelRTConfig.LoggerConfig)
defer logger.GetLoggerInstance().Sync() defer logger.GetLoggerInstance().Sync()
// init OTel TracerProvider
tp, tpErr := middleware.InitTracerProvider(context.Background(), modelRTConfig)
if tpErr != nil {
log.Printf("warn: OTLP tracer init failed, tracing disabled: %v", tpErr)
}
if tp != nil {
defer func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tp.Shutdown(shutdownCtx)
}()
}
ctx, startupSpan := otel.Tracer("modelRT/main").Start(context.Background(), "startup")
defer startupSpan.End()
hostName, err := os.Hostname() hostName, err := os.Hostname()
if err != nil { if err != nil {
logger.Error(ctx, "get host name failed", "error", err) logger.Error(ctx, "get host name failed", "error", err)
@ -226,7 +241,7 @@ func main() {
} }
go realtimedata.StartComputingRealTimeDataLimit(ctx, allMeasurement) go realtimedata.StartComputingRealTimeDataLimit(ctx, allMeasurement)
tree, err := database.QueryTopologicFromDB(ctx, tx) tree, _, err := database.QueryTopologicFromDB(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)
panic(err) panic(err)
@ -285,3 +300,4 @@ func main() {
} }
} }
} }

View File

@ -1,41 +1,93 @@
// Package middleware define gin framework middlewares // Package middleware defines gin framework middlewares and OTel tracing infrastructure.
package middleware package middleware
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"strings" "strings"
"time" "time"
"modelRT/config"
"modelRT/constants" "modelRT/constants"
"modelRT/logger" "modelRT/logger"
"modelRT/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/propagators/b3"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
oteltrace "go.opentelemetry.io/otel/trace"
) )
// StartTrace define func of set trace info from request header // InitTracerProvider creates an OTLP TracerProvider and registers it as the global provider.
func StartTrace() gin.HandlerFunc { // It also registers the B3 propagator to stay compatible with existing B3 infrastructure.
return func(c *gin.Context) { // The caller is responsible for calling Shutdown on the returned provider during graceful shutdown.
traceID := c.Request.Header.Get(constants.HeaderTraceID) func InitTracerProvider(ctx context.Context, cfg config.ModelRTConfig) (*sdktrace.TracerProvider, error) {
parentSpanID := c.Request.Header.Get(constants.HeaderSpanID) opts := []otlptracehttp.Option{
spanID := util.GenerateSpanID(c.Request.RemoteAddr) otlptracehttp.WithEndpoint(cfg.OtelConfig.Endpoint),
// if traceId is empty, it means it is the origin of the link. Set it to the spanId of this time. The originating spanId is the root spanId. }
if traceID == "" { if cfg.OtelConfig.Insecure {
// traceId identifies the entire request link, and spanId identifies the different services in the link. opts = append(opts, otlptracehttp.WithInsecure())
traceID = spanID
} }
c.Set(constants.HeaderTraceID, traceID)
c.Set(constants.HeaderSpanID, spanID)
c.Set(constants.HeaderParentSpanID, parentSpanID)
// also inject into request context so c.Request.Context() carries trace values exporter, err := otlptracehttp.New(ctx, opts...)
reqCtx := c.Request.Context() if err != nil {
reqCtx = context.WithValue(reqCtx, constants.CtxKeyTraceID, traceID) return nil, fmt.Errorf("create OTLP exporter: %w", err)
reqCtx = context.WithValue(reqCtx, constants.CtxKeySpanID, spanID) }
reqCtx = context.WithValue(reqCtx, constants.CtxKeyParentSpanID, parentSpanID)
c.Request = c.Request.WithContext(reqCtx) res := sdkresource.NewSchemaless(
attribute.String("service.name", cfg.ServiceName),
attribute.String("deployment.environment", cfg.DeployEnv),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(b3.New())
return tp, nil
}
// StartTrace extracts upstream B3 trace context from request headers and starts a server span.
// Typed context keys are also injected for backward compatibility with the existing logger
// until the logger is migrated to read from the OTel span context (Step 6).
func StartTrace() gin.HandlerFunc {
tracer := otel.Tracer("modelRT/http")
return func(c *gin.Context) {
// Extract upstream trace context from B3 headers (X-B3-TraceId etc.)
ctx := otel.GetTextMapPropagator().Extract(
c.Request.Context(),
propagation.HeaderCarrier(c.Request.Header),
)
spanName := c.FullPath()
if spanName == "" {
spanName = c.Request.URL.Path
}
ctx, span := tracer.Start(ctx, spanName,
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
)
defer span.End()
// backward compat: inject typed keys so existing logger reads work until Step 6
spanCtx := span.SpanContext()
ctx = context.WithValue(ctx, constants.CtxKeyTraceID, spanCtx.TraceID().String())
ctx = context.WithValue(ctx, constants.CtxKeySpanID, spanCtx.SpanID().String())
c.Request = c.Request.WithContext(ctx)
// set in gin context for accessLog (logger.New(c) reads via gin.Context.Value)
c.Set(constants.HeaderTraceID, spanCtx.TraceID().String())
c.Set(constants.HeaderSpanID, spanCtx.SpanID().String())
c.Next() c.Next()
} }

View File

@ -550,7 +550,6 @@ func handleLevelFuzzySearch(ctx context.Context, rdb *redis.Client, hierarchy co
IsFuzzy: true, IsFuzzy: true,
Err: nil, Err: nil,
} }
return
} }
// runFuzzySearch define func to process redis fuzzy search // runFuzzySearch define func to process redis fuzzy search

View File

@ -62,8 +62,9 @@ type AsyncTaskStatusUpdate struct {
// TopologyAnalysisParams defines the parameters for topology analysis task // TopologyAnalysisParams defines the parameters for topology analysis task
type TopologyAnalysisParams struct { type TopologyAnalysisParams struct {
StartUUID string `json:"start_uuid" example:"comp-001" description:"起始元件UUID"` StartComponentUUID string `json:"start_component_uuid" example:"550e8400-e29b-41d4-a716-446655440000" description:"起始元件UUID"`
EndUUID string `json:"end_uuid" example:"comp-999" description:"目标元件UUID"` EndComponentUUID string `json:"end_component_uuid" example:"550e8400-e29b-41d4-a716-446655440001" description:"目标元件UUID"`
CheckInService bool `json:"check_in_service" example:"true" description:"是否检查路径上元件的投运状态默认为true"`
} }
// PerformanceAnalysisParams defines the parameters for performance analysis task // PerformanceAnalysisParams defines the parameters for performance analysis task

View File

@ -158,7 +158,7 @@ func ConvertComponentUpdateInfosToComponents(updateInfo ComponentUpdateInfo) (*o
// Op: info.Op, // Op: info.Op,
// Tag: info.Tag, // Tag: info.Tag,
// 其他字段可根据需要补充 // 其他字段可根据需要补充
Ts: time.Now(), TS: time.Now(),
} }
return component, nil return component, nil
} }

View File

@ -85,7 +85,6 @@ func (a *AsyncMotor) TableName() string {
// SetComponentID func implement BasicModelInterface interface // SetComponentID func implement BasicModelInterface interface
func (a *AsyncMotor) SetComponentID(componentID int64) { func (a *AsyncMotor) SetComponentID(componentID int64) {
a.ComponentID = componentID a.ComponentID = componentID
return
} }
// ReturnTableName func implement BasicModelInterface interface // ReturnTableName func implement BasicModelInterface interface

View File

@ -72,7 +72,6 @@ func (b *BusbarSection) TableName() string {
// SetComponentID func implement BasicModelInterface interface // SetComponentID func implement BasicModelInterface interface
func (b *BusbarSection) SetComponentID(componentID int64) { func (b *BusbarSection) SetComponentID(componentID int64) {
b.ComponentID = componentID b.ComponentID = componentID
return
} }
// ReturnTableName func implement BasicModelInterface interface // ReturnTableName func implement BasicModelInterface interface

View File

@ -34,7 +34,7 @@ type Bay struct {
DevEtc JSONMap `gorm:"column:dev_etc;type:jsonb;not null;default:'[]'"` DevEtc JSONMap `gorm:"column:dev_etc;type:jsonb;not null;default:'[]'"`
Components []uuid.UUID `gorm:"column:components;type:uuid[];not null;default:'{}'"` Components []uuid.UUID `gorm:"column:components;type:uuid[];not null;default:'{}'"`
Op int `gorm:"column:op;not null;default:-1"` Op int `gorm:"column:op;not null;default:-1"`
Ts time.Time `gorm:"column:ts;type:timestamptz;not null;default:CURRENT_TIMESTAMP"` TS time.Time `gorm:"column:ts;type:timestamptz;not null;default:CURRENT_TIMESTAMP"`
} }
// TableName func respresent return table name of Bay // TableName func respresent return table name of Bay

View File

@ -27,7 +27,7 @@ type Component struct {
Label JSONMap `gorm:"column:label;type:jsonb;not null;default:'{}'"` Label JSONMap `gorm:"column:label;type:jsonb;not null;default:'{}'"`
Context JSONMap `gorm:"column:context;type:jsonb;not null;default:'{}'"` Context JSONMap `gorm:"column:context;type:jsonb;not null;default:'{}'"`
Op int `gorm:"column:op;not null;default:-1"` Op int `gorm:"column:op;not null;default:-1"`
Ts time.Time `gorm:"column:ts;type:timestamptz;not null;default:current_timestamp;autoCreateTime"` TS time.Time `gorm:"column:ts;type:timestamptz;not null;default:current_timestamp;autoCreateTime"`
} }
// TableName func respresent return table name of Component // TableName func respresent return table name of Component

View File

@ -12,7 +12,7 @@ type Grid struct {
Name string `gorm:"column:name"` Name string `gorm:"column:name"`
Description string `gorm:"column:description"` Description string `gorm:"column:description"`
Op int `gorm:"column:op"` Op int `gorm:"column:op"`
Ts time.Time `gorm:"column:ts"` TS time.Time `gorm:"column:ts"`
} }
// TableName func respresent return table name of Grid // TableName func respresent return table name of Grid

View File

@ -20,7 +20,7 @@ type Measurement struct {
BayUUID uuid.UUID `gorm:"column:bay_uuid;type:uuid;not null"` BayUUID uuid.UUID `gorm:"column:bay_uuid;type:uuid;not null"`
ComponentUUID uuid.UUID `gorm:"column:component_uuid;type:uuid;not null"` ComponentUUID uuid.UUID `gorm:"column:component_uuid;type:uuid;not null"`
Op int `gorm:"column:op;not null;default:-1"` Op int `gorm:"column:op;not null;default:-1"`
Ts time.Time `gorm:"column:ts;type:timestamptz;not null;default:CURRENT_TIMESTAMP"` TS time.Time `gorm:"column:ts;type:timestamptz;not null;default:CURRENT_TIMESTAMP"`
} }
// TableName func respresent return table name of Measurement // TableName func respresent return table name of Measurement

View File

@ -12,7 +12,7 @@ type Page struct {
Context JSONMap `gorm:"column:context;type:jsonb;default:'{}'"` Context JSONMap `gorm:"column:context;type:jsonb;default:'{}'"`
Description string `gorm:"column:description"` Description string `gorm:"column:description"`
Op int `gorm:"column:op"` Op int `gorm:"column:op"`
Ts time.Time `gorm:"column:ts"` TS time.Time `gorm:"column:ts"`
} }
// TableName func respresent return table name of Page // TableName func respresent return table name of Page

View File

@ -14,7 +14,7 @@ type Station struct {
Description string `gorm:"column:description"` Description string `gorm:"column:description"`
IsLocal bool `gorm:"column:is_local"` IsLocal bool `gorm:"column:is_local"`
Op int `gorm:"column:op"` Op int `gorm:"column:op"`
Ts time.Time `gorm:"column:ts"` TS time.Time `gorm:"column:ts"`
} }
// TableName func respresent return table name of Station // TableName func respresent return table name of Station

View File

@ -16,7 +16,7 @@ type Topologic struct {
Flag int `gorm:"column:flag"` Flag int `gorm:"column:flag"`
Description string `gorm:"column:description;size:512;not null;default:''"` Description string `gorm:"column:description;size:512;not null;default:''"`
Op int `gorm:"column:op;not null;default:-1"` Op int `gorm:"column:op;not null;default:-1"`
Ts time.Time `gorm:"column:ts;type:timestamptz;not null;default:CURRENT_TIMESTAMP"` TS time.Time `gorm:"column:ts;type:timestamptz;not null;default:CURRENT_TIMESTAMP"`
} }
// TableName func respresent return table name of Page // TableName func respresent return table name of Page

View File

@ -13,7 +13,7 @@ type Zone struct {
Name string `gorm:"column:name"` Name string `gorm:"column:name"`
Description string `gorm:"column:description"` Description string `gorm:"column:description"`
Op int `gorm:"column:op"` Op int `gorm:"column:op"`
Ts time.Time `gorm:"column:ts"` TS time.Time `gorm:"column:ts"`
} }
// TableName func respresent return table name of Zone // TableName func respresent return table name of Zone

View File

@ -14,7 +14,7 @@ type Demo struct {
UIAlarm float32 `gorm:"column:ui_alarm" json:"ui_alarm"` // 低电流告警值 UIAlarm float32 `gorm:"column:ui_alarm" json:"ui_alarm"` // 低电流告警值
OIAlarm float32 `gorm:"column:oi_alarm" json:"oi_alarm"` // 高电流告警值 OIAlarm float32 `gorm:"column:oi_alarm" json:"oi_alarm"` // 高电流告警值
Op int `gorm:"column:op" json:"op"` // 操作人 ID Op int `gorm:"column:op" json:"op"` // 操作人 ID
Ts time.Time `gorm:"column:ts" json:"ts"` // 操作时间 TS time.Time `gorm:"column:ts" json:"ts"` // 操作时间
} }
// TableName func respresent return table name of busbar section // TableName func respresent return table name of busbar section
@ -25,7 +25,6 @@ func (d *Demo) TableName() string {
// SetComponentID func implement BasicModelInterface interface // SetComponentID func implement BasicModelInterface interface
func (d *Demo) SetComponentID(componentID int64) { func (d *Demo) SetComponentID(componentID int64) {
d.ComponentID = componentID d.ComponentID = componentID
return
} }
// ReturnTableName func implement BasicModelInterface interface // ReturnTableName func implement BasicModelInterface interface

View File

@ -5,8 +5,11 @@ import (
"context" "context"
"fmt" "fmt"
"sync" "sync"
"time"
"modelRT/database"
"modelRT/logger" "modelRT/logger"
"modelRT/orm"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"gorm.io/gorm" "gorm.io/gorm"
@ -14,8 +17,8 @@ import (
// TaskHandler defines the interface for task processors // TaskHandler defines the interface for task processors
type TaskHandler interface { type TaskHandler interface {
// Execute processes a task with the given ID and type // Execute processes a task with the given ID, type, and params from the MQ message
Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, db *gorm.DB) error Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, params map[string]any, db *gorm.DB) error
// CanHandle returns true if this handler can process the given task type // CanHandle returns true if this handler can process the given task type
CanHandle(taskType TaskType) bool CanHandle(taskType TaskType) bool
// Name returns the name of the handler for logging and metrics // Name returns the name of the handler for logging and metrics
@ -95,26 +98,205 @@ func NewTopologyAnalysisHandler() *TopologyAnalysisHandler {
} }
} }
// Execute processes a topology analysis task // Execute processes a topology analysis task.
func (h *TopologyAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, db *gorm.DB) error { // Params (all sourced from the MQ message, no DB lookup needed):
logger.Info(ctx, "Starting topology analysis", // - start_component_uuid (string, required): BFS origin
// - end_component_uuid (string, required): reachability target
// - check_in_service (bool, optional, default true): skip out-of-service components
func (h *TopologyAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, params map[string]any, db *gorm.DB) error {
logger.Info(ctx, "topology analysis started", "task_id", taskID)
// Phase 1: parse params from MQ message
startComponentUUID, endComponentUUID, checkInService, err := parseTopologyAnalysisParams(params)
if err != nil {
return fmt.Errorf("invalid topology analysis params: %w", err)
}
logger.Info(ctx, "topology params parsed",
"task_id", taskID, "task_id", taskID,
"task_type", taskType, "start", startComponentUUID,
"end", endComponentUUID,
"check_in_service", checkInService,
) )
// TODO: Implement actual topology analysis logic if err := database.UpdateAsyncTaskProgress(ctx, db, taskID, 20); err != nil {
// This would typically involve: logger.Warn(ctx, "update progress failed", "task_id", taskID, "progress", 20, "error", err)
// 1. Fetching task parameters from database }
// 2. Performing topology analysis (checking for islands, shorts, etc.)
// 3. Storing results in database
// 4. Updating task status
// Simulate work // Phase 2: query topology edges from startComponentUUID, build adjacency list
logger.Info(ctx, "Topology analysis completed", topoEdges, err := database.QueryTopologicByStartUUID(ctx, db, startComponentUUID)
if err != nil {
return fmt.Errorf("query topology from start node: %w", err)
}
// adjacency list: uuid_from → []uuid_to
adjMap := make(map[uuid.UUID][]uuid.UUID, len(topoEdges))
// collect all UUIDs for batch InService query
allUUIDs := make(map[uuid.UUID]struct{}, len(topoEdges)*2)
allUUIDs[startComponentUUID] = struct{}{}
for _, edge := range topoEdges {
adjMap[edge.UUIDFrom] = append(adjMap[edge.UUIDFrom], edge.UUIDTo)
allUUIDs[edge.UUIDFrom] = struct{}{}
allUUIDs[edge.UUIDTo] = struct{}{}
}
if err := database.UpdateAsyncTaskProgress(ctx, db, taskID, 40); err != nil {
logger.Warn(ctx, "update progress failed", "task_id", taskID, "progress", 40, "error", err)
}
// Phase 3: batch-load InService status (only when checkInService is true)
inServiceMap := make(map[uuid.UUID]bool)
if checkInService {
uuidSlice := make([]uuid.UUID, 0, len(allUUIDs))
for id := range allUUIDs {
uuidSlice = append(uuidSlice, id)
}
inServiceMap, err = database.QueryComponentsInServiceByUUIDs(ctx, db, uuidSlice)
if err != nil {
return fmt.Errorf("query component in_service status: %w", err)
}
// check the start node itself before BFS
if !inServiceMap[startComponentUUID] {
return persistTopologyResult(ctx, db, taskID, startComponentUUID, endComponentUUID,
checkInService, false, nil, &startComponentUUID)
}
}
if err := database.UpdateAsyncTaskProgress(ctx, db, taskID, 60); err != nil {
logger.Warn(ctx, "update progress failed", "task_id", taskID, "progress", 60, "error", err)
}
// Phase 4: BFS reachability check
visited := make(map[uuid.UUID]struct{})
parent := make(map[uuid.UUID]uuid.UUID) // for path reconstruction
queue := []uuid.UUID{startComponentUUID}
visited[startComponentUUID] = struct{}{}
isReachable := false
var blockedBy *uuid.UUID
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
if cur == endComponentUUID {
isReachable = true
break
}
for _, next := range adjMap[cur] {
if _, seen := visited[next]; seen {
continue
}
if checkInService && !inServiceMap[next] {
// record first out-of-service blocker but keep searching other branches
if blockedBy == nil {
id := next
blockedBy = &id
}
continue
}
visited[next] = struct{}{}
parent[next] = cur
queue = append(queue, next)
}
}
if err := database.UpdateAsyncTaskProgress(ctx, db, taskID, 80); err != nil {
logger.Warn(ctx, "update progress failed", "task_id", taskID, "progress", 80, "error", err)
}
// Phase 5: reconstruct path (if reachable) and persist result
var path []uuid.UUID
if isReachable {
blockedBy = nil // reachable path found — clear any partial blocker
path = reconstructPath(parent, startComponentUUID, endComponentUUID)
}
return persistTopologyResult(ctx, db, taskID, startComponentUUID, endComponentUUID,
checkInService, isReachable, path, blockedBy)
}
// parseTopologyAnalysisParams extracts and validates the three required fields.
// check_in_service defaults to true when absent.
func parseTopologyAnalysisParams(params map[string]any) (startID, endID uuid.UUID, checkInService bool, err error) {
startStr, ok := params["start_component_uuid"].(string)
if !ok || startStr == "" {
err = fmt.Errorf("missing or invalid start_component_uuid")
return
}
endStr, ok := params["end_component_uuid"].(string)
if !ok || endStr == "" {
err = fmt.Errorf("missing or invalid end_component_uuid")
return
}
startID, err = uuid.FromString(startStr)
if err != nil {
err = fmt.Errorf("parse start_component_uuid %q: %w", startStr, err)
return
}
endID, err = uuid.FromString(endStr)
if err != nil {
err = fmt.Errorf("parse end_component_uuid %q: %w", endStr, err)
return
}
// check_in_service defaults to true
checkInService = true
if v, exists := params["check_in_service"]; exists {
if b, isBool := v.(bool); isBool {
checkInService = b
}
}
return
}
// reconstructPath walks the parent map backwards from end to start.
func reconstructPath(parent map[uuid.UUID]uuid.UUID, start, end uuid.UUID) []uuid.UUID {
var path []uuid.UUID
for cur := end; cur != start; cur = parent[cur] {
path = append(path, cur)
}
path = append(path, start)
// reverse: path was built end→start
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
path[i], path[j] = path[j], path[i]
}
return path
}
// persistTopologyResult serialises the analysis outcome and writes it to async_task_result.
func persistTopologyResult(
ctx context.Context, db *gorm.DB, taskID uuid.UUID,
startID, endID uuid.UUID, checkInService, isReachable bool,
path []uuid.UUID, blockedBy *uuid.UUID,
) error {
pathStrs := make([]string, 0, len(path))
for _, id := range path {
pathStrs = append(pathStrs, id.String())
}
result := orm.JSONMap{
"start_component_uuid": startID.String(),
"end_component_uuid": endID.String(),
"check_in_service": checkInService,
"is_reachable": isReachable,
"path": pathStrs,
"computed_at": time.Now().Unix(),
}
if blockedBy != nil {
result["blocked_by"] = blockedBy.String()
}
if err := database.CreateAsyncTaskResult(ctx, db, taskID, result); err != nil {
return fmt.Errorf("save task result: %w", err)
}
logger.Info(ctx, "topology analysis completed",
"task_id", taskID, "task_id", taskID,
"task_type", taskType, "is_reachable", isReachable,
"path_length", len(path),
) )
return nil return nil
} }
@ -136,7 +318,7 @@ func NewEventAnalysisHandler() *EventAnalysisHandler {
} }
// Execute processes an event analysis task // Execute processes an event analysis task
func (h *EventAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, db *gorm.DB) error { func (h *EventAnalysisHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, params map[string]any, db *gorm.DB) error {
logger.Info(ctx, "Starting event analysis", logger.Info(ctx, "Starting event analysis",
"task_id", taskID, "task_id", taskID,
"task_type", taskType, "task_type", taskType,
@ -176,7 +358,7 @@ func NewBatchImportHandler() *BatchImportHandler {
} }
// Execute processes a batch import task // Execute processes a batch import task
func (h *BatchImportHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, db *gorm.DB) error { func (h *BatchImportHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, params map[string]any, db *gorm.DB) error {
logger.Info(ctx, "Starting batch import", logger.Info(ctx, "Starting batch import",
"task_id", taskID, "task_id", taskID,
"task_type", taskType, "task_type", taskType,
@ -214,13 +396,13 @@ func NewCompositeHandler(factory *HandlerFactory) *CompositeHandler {
} }
// Execute delegates task execution to the appropriate handler // Execute delegates task execution to the appropriate handler
func (h *CompositeHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, db *gorm.DB) error { func (h *CompositeHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, params map[string]any, db *gorm.DB) error {
handler, err := h.factory.GetHandler(taskType) handler, err := h.factory.GetHandler(taskType)
if err != nil { if err != nil {
return fmt.Errorf("failed to get handler for task type %s: %w", taskType, err) return fmt.Errorf("failed to get handler for task type %s: %w", taskType, err)
} }
return handler.Execute(ctx, taskID, taskType, db) return handler.Execute(ctx, taskID, taskType, params, db)
} }
// CanHandle returns true if any registered handler can handle the task type // CanHandle returns true if any registered handler can handle the task type

View File

@ -14,8 +14,8 @@ type TaskQueueMessage struct {
TaskID uuid.UUID `json:"task_id"` TaskID uuid.UUID `json:"task_id"`
TaskType TaskType `json:"task_type"` TaskType TaskType `json:"task_type"`
Priority int `json:"priority,omitempty"` // Optional, defaults to constants.TaskPriorityDefault Priority int `json:"priority,omitempty"` // Optional, defaults to constants.TaskPriorityDefault
TraceID string `json:"trace_id,omitempty"` // propagated from the originating HTTP request TraceCarrier map[string]string `json:"trace_carrier,omitempty"` // OTel propagation carrier (B3 headers)
SpanID string `json:"span_id,omitempty"` // spanID of the step that enqueued this message Params map[string]any `json:"params,omitempty"` // Task-specific parameters, set by the HTTP handler
} }
// NewTaskQueueMessage creates a new TaskQueueMessage with default priority // NewTaskQueueMessage creates a new TaskQueueMessage with default priority

View File

@ -11,10 +11,13 @@ import (
"modelRT/constants" "modelRT/constants"
"modelRT/logger" "modelRT/logger"
"modelRT/mq" "modelRT/mq"
"modelRT/util"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
oteltrace "go.opentelemetry.io/otel/trace"
) )
// TaskMsgChan buffers task messages to be published to RabbitMQ asynchronously // TaskMsgChan buffers task messages to be published to RabbitMQ asynchronously
@ -115,6 +118,11 @@ func (p *QueueProducer) PublishTask(ctx context.Context, taskID uuid.UUID, taskT
return fmt.Errorf("invalid task message: taskID=%s, taskType=%s", taskID, taskType) return fmt.Errorf("invalid task message: taskID=%s, taskType=%s", taskID, taskType)
} }
// Inject OTel trace context so the consumer (worker) can restore the span chain
carrier := make(map[string]string)
otel.GetTextMapPropagator().Inject(ctx, propagation.MapCarrier(carrier))
message.TraceCarrier = carrier
// Convert message to JSON // Convert message to JSON
body, err := json.Marshal(message) body, err := json.Marshal(message)
if err != nil { if err != nil {
@ -242,17 +250,17 @@ func PushTaskToRabbitMQ(ctx context.Context, cfg config.RabbitMQConfig, taskChan
logger.Info(ctx, "task channel closed, exiting push loop") logger.Info(ctx, "task channel closed, exiting push loop")
return return
} }
traceID := msg.TraceID // Restore trace context from the handler that enqueued this message
if traceID == "" { taskCtx := otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier(msg.TraceCarrier))
traceID = msg.TaskID.String() // fallback when no HTTP trace was propagated taskCtx, pubSpan := otel.Tracer("modelRT/task").Start(taskCtx, "task.publish",
} oteltrace.WithAttributes(attribute.String("task_id", msg.TaskID.String())),
taskCtx := context.WithValue(ctx, constants.CtxKeyTraceID, traceID) )
taskCtx = context.WithValue(taskCtx, constants.CtxKeySpanID, util.GenerateSpanID("task-publish"))
taskCtx = context.WithValue(taskCtx, constants.CtxKeyParentSpanID, msg.SpanID)
if err := producer.PublishTaskWithRetry(taskCtx, msg.TaskID, msg.TaskType, msg.Priority, 3); err != nil { if err := producer.PublishTaskWithRetry(taskCtx, msg.TaskID, msg.TaskType, msg.Priority, 3); err != nil {
pubSpan.RecordError(err)
logger.Error(taskCtx, "publish task to RabbitMQ failed", logger.Error(taskCtx, "publish task to RabbitMQ failed",
"task_id", msg.TaskID, "error", err) "task_id", msg.TaskID, "error", err)
} }
pubSpan.End()
} }
} }
} }

View File

@ -141,26 +141,20 @@ func NewTestTaskHandler() *TestTaskHandler {
} }
// Execute processes a test task using the unified task interface // Execute processes a test task using the unified task interface
func (h *TestTaskHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, db *gorm.DB) error { func (h *TestTaskHandler) Execute(ctx context.Context, taskID uuid.UUID, taskType TaskType, params map[string]any, db *gorm.DB) error {
logger.Info(ctx, "Executing test task", logger.Info(ctx, "Executing test task",
"task_id", taskID, "task_id", taskID,
"task_type", taskType, "task_type", taskType,
) )
// Fetch task parameters from database // Convert params from MQ message to TestTaskParams
asyncTask, err := database.GetAsyncTaskByID(ctx, db, taskID) taskParams := &TestTaskParams{}
if err != nil { if err := taskParams.FromMap(params); err != nil {
return fmt.Errorf("failed toser fetch task: %w", err)
}
// Convert params map to TestTaskParams
params := &TestTaskParams{}
if err := params.FromMap(map[string]interface{}(asyncTask.Params)); err != nil {
return fmt.Errorf("failed to parse task params: %w", err) return fmt.Errorf("failed to parse task params: %w", err)
} }
// Create and execute test task // Create and execute test task
testTask := NewTestTask(*params) testTask := NewTestTask(*taskParams)
return testTask.Execute(ctx, taskID, db) return testTask.Execute(ctx, taskID, db)
} }

View File

@ -14,11 +14,14 @@ import (
"modelRT/logger" "modelRT/logger"
"modelRT/mq" "modelRT/mq"
"modelRT/orm" "modelRT/orm"
"modelRT/util"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/panjf2000/ants/v2" "github.com/panjf2000/ants/v2"
amqp "github.com/rabbitmq/amqp091-go" amqp "github.com/rabbitmq/amqp091-go"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
oteltrace "go.opentelemetry.io/otel/trace"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -283,14 +286,15 @@ func (w *TaskWorker) handleMessage(msg amqp.Delivery) {
return return
} }
// derive a per-task context carrying the trace propagated from the originating HTTP request // Restore trace context from the publish span, then create an execute child span
traceID := taskMsg.TraceID taskCtx := otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier(taskMsg.TraceCarrier))
if traceID == "" { taskCtx, span := otel.Tracer("modelRT/task").Start(taskCtx, "task.execute",
traceID = taskMsg.TaskID.String() // fallback when message carries no trace oteltrace.WithAttributes(
} attribute.String("task_id", taskMsg.TaskID.String()),
taskCtx := context.WithValue(ctx, constants.CtxKeyTraceID, traceID) attribute.String("task_type", string(taskMsg.TaskType)),
taskCtx = context.WithValue(taskCtx, constants.CtxKeySpanID, util.GenerateSpanID("task-worker")) ),
taskCtx = context.WithValue(taskCtx, constants.CtxKeyParentSpanID, taskMsg.SpanID) )
defer span.End()
ctx = taskCtx ctx = taskCtx
logger.Info(ctx, "Processing task", logger.Info(ctx, "Processing task",
@ -312,7 +316,7 @@ func (w *TaskWorker) handleMessage(msg amqp.Delivery) {
// Execute task using handler // Execute task using handler
startTime := time.Now() startTime := time.Now()
err := w.handler.Execute(ctx, taskMsg.TaskID, taskMsg.TaskType, w.db) err := w.handler.Execute(ctx, taskMsg.TaskID, taskMsg.TaskType, taskMsg.Params, w.db)
processingTime := time.Since(startTime) processingTime := time.Since(startTime)
if err != nil { if err != nil {