package environment import ( "context" "database/sql" "encoding/json" "errors" "time" ) // GatewayTaskStatus 是任务生命周期;dispatched 后连接中断标记 failed,不自动重发。 const ( TaskPending = "pending" TaskDispatched = "dispatched" TaskSucceeded = "succeeded" TaskFailed = "failed" ) // GatewayTask 是一次平台→网关通信的持久化记录,全量任务即执行记录。 type GatewayTask struct { ID int64 `json:"id"` GatewayID int64 `json:"gateway_id"` Method string `json:"method"` Path string `json:"path"` Payload json.RawMessage `json:"payload"` Status string `json:"status"` ResponseStatus int `json:"response_status"` Error string `json:"error"` ResponseBytes int64 `json:"response_bytes"` TimeoutSeconds int `json:"timeout_seconds"` CreatedAt time.Time `json:"created_at"` DispatchedAt *time.Time `json:"dispatched_at"` FinishedAt *time.Time `json:"finished_at"` } func validGatewayMethod(method string) bool { switch method { case "GET", "POST", "PUT", "DELETE": return true } return false } const gatewayTaskColumns = `id, gateway_id, method, path, payload, status, response_status, error, response_bytes, timeout_seconds, created_at, dispatched_at, finished_at` func scanGatewayTask(row rowScanner) (GatewayTask, error) { var task GatewayTask var dispatched, finished sql.NullTime if err := row.Scan(&task.ID, &task.GatewayID, &task.Method, &task.Path, &task.Payload, &task.Status, &task.ResponseStatus, &task.Error, &task.ResponseBytes, &task.TimeoutSeconds, &task.CreatedAt, &dispatched, &finished); err != nil { return GatewayTask{}, rowError(err) } if dispatched.Valid { task.DispatchedAt = &dispatched.Time } if finished.Valid { task.FinishedAt = &finished.Time } return task, nil } // CreateGatewayTask 入队一个任务;payload 必须是合法 JSON。 func (s *Store) CreateGatewayTask(ctx context.Context, gatewayID int64, method, path string, payload []byte, timeout time.Duration) (GatewayTask, error) { if gatewayID <= 0 || !validGatewayMethod(method) || path == "" || len(path) > 512 || !json.Valid(payload) { return GatewayTask{}, ErrInvalid } seconds := int(timeout / time.Second) if seconds < 1 || seconds > 600 { return GatewayTask{}, ErrInvalid } if len(payload) == 0 { payload = []byte(`{}`) } return scanGatewayTask(s.db.QueryRowContext(ctx, ` INSERT INTO gateway_task (gateway_id, method, path, payload, timeout_seconds) VALUES ($1, $2, $3, $4, $5) RETURNING `+gatewayTaskColumns, gatewayID, method, path, payload, seconds)) } // DispatchGatewayTask 抢占一个待下发任务;仅当仍为 pending 时成功,保证单次下发。 func (s *Store) DispatchGatewayTask(ctx context.Context, taskID int64) (bool, error) { result, err := s.db.ExecContext(ctx, ` UPDATE gateway_task SET status = 'dispatched', dispatched_at = now() WHERE id = $1 AND status = 'pending'`, taskID) if err != nil { return false, err } count, err := result.RowsAffected() if err != nil { return false, err } return count == 1, nil } // FinishGatewayTask 记录任务终态;dispatched 中任务由结果更新,pending 任务允许直接终态(网关离线),重复投递幂等忽略。 func (s *Store) FinishGatewayTask(ctx context.Context, taskID int64, status string, responseStatus int, taskError string, responseBytes int64) (bool, error) { if status != TaskSucceeded && status != TaskFailed { return false, ErrInvalid } if len(taskError) > 2048 { taskError = taskError[:2048] } result, err := s.db.ExecContext(ctx, ` UPDATE gateway_task SET status = $2, response_status = $3, error = $4, response_bytes = $5, finished_at = now() WHERE id = $1 AND status IN ('dispatched', 'pending')`, taskID, status, responseStatus, taskError, responseBytes) if err != nil { return false, err } count, err := result.RowsAffected() if err != nil { return false, err } return count == 1, nil } // InterruptDispatchedTasks 连接断开时把该网关在途任务标记失败(结果未确认,不自动重发)。 func (s *Store) InterruptDispatchedTasks(ctx context.Context, gatewayID int64, reason string) error { if len(reason) > 2048 { reason = reason[:2048] } _, err := s.db.ExecContext(ctx, ` UPDATE gateway_task SET status = 'failed', error = $2, finished_at = now() WHERE gateway_id = $1 AND status = 'dispatched'`, gatewayID, reason) return err } // ResetGatewayConnections 平台启动时清空内存会话遗留的在线标记。 func (s *Store) ResetGatewayConnections(ctx context.Context) error { _, err := s.db.ExecContext(ctx, `UPDATE gateway SET online = false, version = '' WHERE online`) return err } // NextPendingGatewayTasks 读取某网关按 id 序的待下发任务(补发队列)。 func (s *Store) NextPendingGatewayTasks(ctx context.Context, gatewayID int64, limit int) ([]GatewayTask, error) { if gatewayID <= 0 || limit <= 0 { return nil, ErrInvalid } rows, err := s.db.QueryContext(ctx, ` SELECT `+gatewayTaskColumns+` FROM gateway_task WHERE gateway_id = $1 AND status = 'pending' ORDER BY id LIMIT $2`, gatewayID, limit) if err != nil { return nil, err } defer rows.Close() tasks := []GatewayTask{} for rows.Next() { task, err := scanGatewayTask(rows) if err != nil { return nil, err } tasks = append(tasks, task) } return tasks, rows.Err() } type GatewayTaskPage struct { Data []GatewayTask `json:"data"` Page int `json:"page"` PageSize int `json:"page_size"` Total int `json:"total"` } // ListGatewayTasksPage 分页读取网关执行记录,最新创建在前。 func (s *Store) ListGatewayTasksPage(ctx context.Context, gatewayID int64, page, pageSize int) (GatewayTaskPage, error) { if gatewayID <= 0 { return GatewayTaskPage{}, ErrInvalid } if page < 1 || pageSize < 1 || pageSize > 100 { return GatewayTaskPage{}, ErrInvalid } result := GatewayTaskPage{Data: []GatewayTask{}, Page: page, PageSize: pageSize} if err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM gateway_task WHERE gateway_id = $1`, gatewayID).Scan(&result.Total); err != nil { return result, err } rows, err := s.db.QueryContext(ctx, ` SELECT `+gatewayTaskColumns+` FROM gateway_task WHERE gateway_id = $1 ORDER BY id DESC LIMIT $2 OFFSET $3`, gatewayID, pageSize, (page-1)*pageSize) if err != nil { return result, err } defer rows.Close() for rows.Next() { task, err := scanGatewayTask(rows) if err != nil { return result, err } result.Data = append(result.Data, task) } return result, rows.Err() } // CountPendingGatewayTasks 返回各网关待执行任务数(队列状态展示)。 func (s *Store) CountPendingGatewayTasks(ctx context.Context, gatewayID int64) (int, error) { var count int if err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM gateway_task WHERE gateway_id = $1 AND status = 'pending'`, gatewayID).Scan(&count); err != nil { return 0, err } return count, nil } var ErrGatewayOffline = errors.New("网关离线,无法执行任务")