主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批: 已推库 16 个文件 + 本批剩余文件)。 校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0); 全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
188 lines
7.5 KiB
Go
188 lines
7.5 KiB
Go
package gateway
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations"
|
|
)
|
|
|
|
// IoT 设备控制与自动化规则管理写操作(仅 owner,带审计)。
|
|
// 设备控制会在所属 provider 上执行真实控制;其余在没有
|
|
// 接线场景/规则存储时诚实作答。
|
|
|
|
// iotControlRequest 是 POST /api/v1/admin/iot/{id}/control 的请求体。
|
|
type iotControlRequest struct {
|
|
Action string `json:"action"`
|
|
Params map[string]any `json:"params"`
|
|
}
|
|
|
|
// adminIoTControl 经 IoT 管理器向设备发送控制指令,管理器
|
|
// 在所属 provider 上施加它并发布 device_update
|
|
// 事件。只有部署了设备 provider 时才是真实动作。
|
|
func (s *Server) adminIoTControl(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
var req iotControlRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Action) == "" {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "action is required"))
|
|
return
|
|
}
|
|
mgr, ok := s.iot.(*iot.Manager)
|
|
if !ok {
|
|
s.auditClaims(r, "iot.control", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "iot_unavailable", "IoT 服务未配置"))
|
|
return
|
|
}
|
|
dev, err := mgr.Control(r.Context(), id, req.Action, req.Params)
|
|
if err != nil {
|
|
s.auditClaims(r, "iot.control", "error")
|
|
writeError(w, mapIotError(err))
|
|
return
|
|
}
|
|
if dev == nil {
|
|
s.auditClaims(r, "iot.control", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "device %q not found", id))
|
|
return
|
|
}
|
|
s.auditClaims(r, "iot.control", "ok")
|
|
writeJSON(w, http.StatusOK, iotDeviceSummary{ID: dev.ID, Name: dev.Name, Type: dev.Type, State: dev.State, Online: dev.Online})
|
|
}
|
|
|
|
// adminIoTScenario 诚实作答:IoT 子系统只管设备
|
|
// 列表/控制/状态(Provider 接口没有场景模型或
|
|
// 存储),因此没有可持久化场景的地方。它报告缺失的
|
|
// 存储,而不是编造一个。
|
|
func (s *Server) adminIoTScenario(w http.ResponseWriter, r *http.Request) {
|
|
s.auditClaims(r, "iot.scenario", "error")
|
|
writeError(w, &apperr.AppError{
|
|
Code: "not_implemented",
|
|
Message: "IoT 场景未实现:IoT 子系统仅有设备列表/控制/状态(Provider 接口无场景模型与存储);场景 CRUD 需先新增模型 + 执行器",
|
|
HTTPCode: http.StatusNotImplemented,
|
|
})
|
|
}
|
|
|
|
// adminIoTRule 诚实作答:不存在 IoT 自动化规则模型或存储
|
|
// (通用 automations 服务运行 cron/event 规则,而不是带设备动作的
|
|
// 设备状态条件),因此规则无法被持久化或执行。
|
|
func (s *Server) adminIoTRule(w http.ResponseWriter, r *http.Request) {
|
|
s.auditClaims(r, "iot.rule", "error")
|
|
writeError(w, &apperr.AppError{
|
|
Code: "not_implemented",
|
|
Message: "IoT 自动化规则未实现:IoT 子系统无规则模型与存储(通用 automations 服务只跑 cron/事件规则,不含设备状态条件 + 设备动作执行器)",
|
|
HTTPCode: http.StatusNotImplemented,
|
|
})
|
|
}
|
|
|
|
// mapIotError 把 iot 哨兵错误映射为 HTTP 错误。
|
|
func mapIotError(err error) *apperr.AppError {
|
|
switch {
|
|
case errors.Is(err, iot.ErrDeviceNotFound):
|
|
return apperr.Wrap(apperr.ErrNotFound, "not_found", "device not found")
|
|
case errors.Is(err, iot.ErrUnsupported):
|
|
return apperr.Wrap(apperr.ErrBadRequest, "unsupported_action", "action not supported")
|
|
case errors.Is(err, iot.ErrOffline):
|
|
return apperr.Wrap(apperr.ErrConflict, "device_offline", "device offline")
|
|
case errors.Is(err, iot.ErrInvalidParam):
|
|
return apperr.Wrap(apperr.ErrBadRequest, "invalid_param", "invalid parameter")
|
|
default:
|
|
return apperr.Wrap(apperr.ErrInternal, "iot_control_failed", "device control failed")
|
|
}
|
|
}
|
|
|
|
// automationCreateRequest 是 POST /api/v1/admin/automations 的请求体。
|
|
type automationCreateRequest struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"` // cron | event
|
|
Expression string `json:"expression"`
|
|
}
|
|
|
|
// adminCreateAutomation 注册一条真实的 cron/event 规则,其动作会记录
|
|
// 触发。无效的 cron 表达式或畸形的规则会被诚实作答。
|
|
func (s *Server) adminCreateAutomation(w http.ResponseWriter, r *http.Request) {
|
|
var req automationCreateRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Kind) == "" || strings.TrimSpace(req.Expression) == "" {
|
|
s.auditClaims(r, "automation.create", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "name, kind and expression are required"))
|
|
return
|
|
}
|
|
auto, ok := s.automations.(*automations.Service)
|
|
if !ok {
|
|
s.auditClaims(r, "automation.create", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_unavailable", "自动化服务未配置"))
|
|
return
|
|
}
|
|
id, err := auto.AddRuleByName(r.Context(), req.Name, req.Kind, req.Expression)
|
|
if err != nil {
|
|
s.auditClaims(r, "automation.create", "error")
|
|
if errors.Is(err, automations.ErrInvalidCron) {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "invalid_cron", "invalid cron expression"))
|
|
return
|
|
}
|
|
if errors.Is(err, automations.ErrInvalidRule) {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "invalid_rule", "invalid rule"))
|
|
return
|
|
}
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_failed", "failed to add automation"))
|
|
return
|
|
}
|
|
s.auditClaims(r, "automation.create", "ok")
|
|
writeJSON(w, http.StatusCreated, map[string]any{"id": id, "name": req.Name, "kind": req.Kind})
|
|
}
|
|
|
|
// adminRunAutomation 手动触发一次已注册规则的动作。
|
|
func (s *Server) adminRunAutomation(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
auto, ok := s.automations.(*automations.Service)
|
|
if !ok {
|
|
s.auditClaims(r, "automation.run", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_unavailable", "自动化服务未配置"))
|
|
return
|
|
}
|
|
out, err := auto.RunRule(r.Context(), id)
|
|
if err != nil {
|
|
s.auditClaims(r, "automation.run", "error")
|
|
if errors.Is(err, automations.ErrNotFound) {
|
|
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "automation %q not found", id))
|
|
return
|
|
}
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_failed", "failed to run automation"))
|
|
return
|
|
}
|
|
s.auditClaims(r, "automation.run", "ok")
|
|
writeJSON(w, http.StatusOK, map[string]any{"id": id, "output": out})
|
|
}
|
|
|
|
// adminDeleteAutomation 按 id 移除一条已注册规则(cron 或 event)。
|
|
func (s *Server) adminDeleteAutomation(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
auto, ok := s.automations.(*automations.Service)
|
|
if !ok {
|
|
s.auditClaims(r, "automation.delete", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_unavailable", "自动化服务未配置"))
|
|
return
|
|
}
|
|
if err := auto.RemoveRule(r.Context(), id); err != nil {
|
|
s.auditClaims(r, "automation.delete", "error")
|
|
if errors.Is(err, automations.ErrNotFound) {
|
|
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "automation %q not found", id))
|
|
return
|
|
}
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_failed", "failed to delete automation"))
|
|
return
|
|
}
|
|
s.auditClaims(r, "automation.delete", "ok")
|
|
writeJSON(w, http.StatusOK, map[string]any{"id": id, "deleted": true})
|
|
}
|