745 lines
22 KiB
Go
745 lines
22 KiB
Go
package api
|
|
|
|
import (
|
|
"aquacontrolai/internal/model"
|
|
"aquacontrolai/internal/pkg/response"
|
|
postgres "aquacontrolai/internal/repository/postgres"
|
|
"aquacontrolai/internal/service/platform"
|
|
"context"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Handler struct {
|
|
Platform *platform.Service
|
|
History *platform.History
|
|
}
|
|
|
|
func NewRouter(h *Handler) *gin.Engine {
|
|
r := gin.New()
|
|
r.Use(gin.Recovery(), requestLog())
|
|
v := r.Group("/api/v1")
|
|
v.GET("/health", h.health)
|
|
v.GET("/devices/protocols", h.protocols)
|
|
v.GET("/devices", h.devices)
|
|
v.POST("/devices/export", h.exportConfig("device"))
|
|
v.POST("/devices/import", h.importConfig("device"))
|
|
v.POST("/devices", h.createDevice)
|
|
v.GET("/devices/:id", h.deviceDetail)
|
|
v.PUT("/devices/:id", h.updateDevice)
|
|
v.DELETE("/devices/:id", h.deleteDevice)
|
|
v.GET("/collection-points", h.points("collection"))
|
|
v.GET("/collection-points/groups", h.groups)
|
|
v.POST("/collection-points/groups", h.createGroup)
|
|
v.PUT("/collection-points/groups", h.updateGroup)
|
|
v.DELETE("/collection-points/groups/:name", h.deleteGroup)
|
|
v.POST("/collection-points/export", h.exportConfig("collection"))
|
|
v.POST("/collection-points/import", h.importConfig("collection"))
|
|
v.GET("/collection-points/:id", h.pointDetail("collection"))
|
|
v.POST("/collection-points", h.savePoint("collection", false))
|
|
v.PUT("/collection-points/:id", h.savePoint("collection", true))
|
|
v.DELETE("/collection-points/:id", h.deletePoint("collection"))
|
|
v.GET("/write-points", h.points("write"))
|
|
v.POST("/write-points/export", h.exportConfig("write"))
|
|
v.POST("/write-points/import", h.importConfig("write"))
|
|
v.GET("/write-points/:id", h.pointDetail("write"))
|
|
v.POST("/write-points", h.savePoint("write", false))
|
|
v.PUT("/write-points/:id", h.savePoint("write", true))
|
|
v.DELETE("/write-points/:id", h.deletePoint("write"))
|
|
v.POST("/write-points/:id/write", h.writePoint)
|
|
v.GET("/write-logs", h.logs)
|
|
v.GET("/history/tree", h.tree)
|
|
v.POST("/history/archive/cleanup", h.cleanupHistoryArchive)
|
|
v.POST("/history/query", h.historyQuery)
|
|
v.POST("/history/query-table", h.historyTable)
|
|
v.POST("/history/export", h.exportHistory)
|
|
v.GET("/system/history-retention", h.getRetention)
|
|
v.PUT("/system/history-retention", h.setRetention)
|
|
return r
|
|
}
|
|
func requestLog() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
start := time.Now()
|
|
c.Next()
|
|
slog.Info("HTTP 请求", "method", c.Request.Method, "path", c.Request.URL.Path, "status", c.Writer.Status(), "duration_ms", time.Since(start).Milliseconds())
|
|
}
|
|
}
|
|
func (h *Handler) health(c *gin.Context) { response.OK(c, map[string]string{"status": "ok"}) }
|
|
func (h *Handler) protocols(c *gin.Context) {
|
|
response.OK(c, map[string]any{"items": h.Platform.Registry.Metadata()})
|
|
}
|
|
func (h *Handler) devices(c *gin.Context) {
|
|
items, e := h.Platform.ListDevices(c, c.Query("keyword"))
|
|
if e != nil {
|
|
serverError(c, e)
|
|
return
|
|
}
|
|
filtered := make([]model.Device, 0, len(items))
|
|
for _, d := range items {
|
|
if v := c.Query("protocol_type"); v != "" && d.ProtocolType != v {
|
|
continue
|
|
}
|
|
if v := c.Query("enabled"); v != "" && strconv.FormatBool(d.Enabled) != v {
|
|
continue
|
|
}
|
|
filtered = append(filtered, d)
|
|
}
|
|
pageNo, pageSize := pagination(c)
|
|
start, end := bounds(len(filtered), pageNo, pageSize)
|
|
response.OK(c, map[string]any{"total": len(filtered), "page": pageNo, "page_size": pageSize, "items": filtered[start:end]})
|
|
}
|
|
func (h *Handler) deviceDetail(c *gin.Context) {
|
|
id, e := uuid.Parse(c.Param("id"))
|
|
if e != nil {
|
|
response.Error(c, 400, 40001, "无效ID", nil)
|
|
return
|
|
}
|
|
d, e := h.Platform.GetDevice(c, id)
|
|
if e != nil {
|
|
response.Error(c, 404, 40004, "设备不存在", nil)
|
|
return
|
|
}
|
|
response.OK(c, d)
|
|
}
|
|
func bindDevice(c *gin.Context) (model.Device, error) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
ProtocolType string `json:"protocol_type"`
|
|
Enabled *bool `json:"enabled"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
ConnectTimeout int `json:"connect_timeout"`
|
|
ReconnectInterval int `json:"reconnect_interval"`
|
|
ProtocolConfig json.RawMessage `json:"protocol_config"`
|
|
}
|
|
if e := c.ShouldBindJSON(&req); e != nil {
|
|
return model.Device{}, e
|
|
}
|
|
enabled := true
|
|
if req.Enabled != nil {
|
|
enabled = *req.Enabled
|
|
}
|
|
return model.Device{Name: req.Name, ProtocolType: req.ProtocolType, Enabled: enabled, Host: req.Host, Port: req.Port, ConnectTimeout: req.ConnectTimeout, ReconnectInterval: req.ReconnectInterval, ProtocolConfig: req.ProtocolConfig}, nil
|
|
}
|
|
func (h *Handler) createDevice(c *gin.Context) {
|
|
d, e := bindDevice(c)
|
|
if e == nil {
|
|
e = h.Platform.SaveDevice(c, nil, &d)
|
|
}
|
|
if e != nil {
|
|
response.Error(c, http.StatusBadRequest, 40001, e.Error(), nil)
|
|
return
|
|
}
|
|
response.Created(c, d)
|
|
}
|
|
func (h *Handler) updateDevice(c *gin.Context) {
|
|
id, e := uuid.Parse(c.Param("id"))
|
|
d, e2 := bindDevice(c)
|
|
if e != nil || e2 != nil {
|
|
response.Error(c, 400, 40001, "无效请求", nil)
|
|
return
|
|
}
|
|
if e = h.Platform.SaveDevice(c, &id, &d); e != nil {
|
|
response.Error(c, 400, 40001, e.Error(), nil)
|
|
return
|
|
}
|
|
response.OK(c, d)
|
|
}
|
|
func (h *Handler) deleteDevice(c *gin.Context) {
|
|
id, e := uuid.Parse(c.Param("id"))
|
|
if e != nil {
|
|
response.Error(c, 400, 40001, "无效ID", nil)
|
|
return
|
|
}
|
|
if e = h.Platform.DeleteDevice(c, id); e != nil {
|
|
response.Error(c, 404, 40004, "设备不存在", nil)
|
|
return
|
|
}
|
|
response.OK(c, nil)
|
|
}
|
|
func (h *Handler) points(kind string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
items, e := h.Platform.ListPoints(c, kind, c.Query("keyword"))
|
|
if e != nil {
|
|
serverError(c, e)
|
|
return
|
|
}
|
|
filtered := make([]postgres.PointRow, 0, len(items))
|
|
for _, p := range items {
|
|
if v := c.Query("device_id"); v != "" && p.DeviceID.String() != v {
|
|
continue
|
|
}
|
|
if v := c.Query("group_name"); v != "" && p.GroupName != v {
|
|
continue
|
|
}
|
|
if v := c.Query("data_type"); v != "" && p.DataType != v {
|
|
continue
|
|
}
|
|
if kind == "collection" {
|
|
if v := c.Query("enabled"); v != "" && strconv.FormatBool(p.Enabled) != v {
|
|
continue
|
|
}
|
|
}
|
|
if kind == "write" {
|
|
if v := c.Query("write_enabled"); v != "" && strconv.FormatBool(p.WriteEnabled) != v {
|
|
continue
|
|
}
|
|
}
|
|
filtered = append(filtered, p)
|
|
}
|
|
pageNo, pageSize := pagination(c)
|
|
start, end := bounds(len(filtered), pageNo, pageSize)
|
|
response.OK(c, map[string]any{"total": len(filtered), "page": pageNo, "page_size": pageSize, "items": filtered[start:end]})
|
|
}
|
|
}
|
|
func pagination(c *gin.Context) (int, int) {
|
|
pageNo := parseInt(c.DefaultQuery("page", "1"), 1)
|
|
pageSize := parseInt(c.DefaultQuery("page_size", "20"), 20)
|
|
if pageNo < 1 {
|
|
pageNo = 1
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
return pageNo, pageSize
|
|
}
|
|
func bounds(total, pageNo, pageSize int) (int, int) {
|
|
start := (pageNo - 1) * pageSize
|
|
if start > total {
|
|
start = total
|
|
}
|
|
end := start + pageSize
|
|
if end > total {
|
|
end = total
|
|
}
|
|
return start, end
|
|
}
|
|
func (h *Handler) pointDetail(kind string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, e := uuid.Parse(c.Param("id"))
|
|
if e != nil {
|
|
response.Error(c, 400, 41001, "无效ID", nil)
|
|
return
|
|
}
|
|
p, e := h.Platform.GetPoint(c, kind, id)
|
|
if e != nil {
|
|
response.Error(c, 404, 41004, "点位不存在", nil)
|
|
return
|
|
}
|
|
response.OK(c, p)
|
|
}
|
|
}
|
|
func (h *Handler) groups(c *gin.Context) {
|
|
groups, e := h.Platform.Groups(c)
|
|
if e != nil {
|
|
serverError(c, e)
|
|
return
|
|
}
|
|
response.OK(c, map[string]any{"groups": groups})
|
|
}
|
|
func (h *Handler) createGroup(c *gin.Context) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if e := c.ShouldBindJSON(&req); e != nil {
|
|
response.Error(c, 400, 41001, "参数格式错误", nil)
|
|
return
|
|
}
|
|
if e := h.Platform.CreateGroup(c, req.Name); e != nil {
|
|
response.Error(c, 400, 41001, e.Error(), nil)
|
|
return
|
|
}
|
|
response.Created(c, map[string]string{"name": req.Name})
|
|
}
|
|
func (h *Handler) updateGroup(c *gin.Context) {
|
|
var req struct {
|
|
OldName string `json:"old_name"`
|
|
Name string `json:"name"`
|
|
}
|
|
if e := c.ShouldBindJSON(&req); e != nil {
|
|
response.Error(c, 400, 41001, "参数格式错误", nil)
|
|
return
|
|
}
|
|
if e := h.Platform.UpdateGroup(c, req.OldName, req.Name); e != nil {
|
|
response.Error(c, 400, 41001, e.Error(), nil)
|
|
return
|
|
}
|
|
response.OK(c, map[string]string{"name": req.Name})
|
|
}
|
|
func (h *Handler) deleteGroup(c *gin.Context) {
|
|
name := c.Param("name")
|
|
if e := h.Platform.DeleteGroup(c, name); e != nil {
|
|
response.Error(c, 409, 41003, e.Error(), nil)
|
|
return
|
|
}
|
|
response.OK(c, nil)
|
|
}
|
|
func (h *Handler) getRetention(c *gin.Context) {
|
|
days, e := h.Platform.GetRetention(c)
|
|
if e != nil {
|
|
serverError(c, e)
|
|
return
|
|
}
|
|
response.OK(c, map[string]int{"history_retention_days": days})
|
|
}
|
|
func (h *Handler) setRetention(c *gin.Context) {
|
|
var req struct {
|
|
Days int `json:"history_retention_days"`
|
|
}
|
|
if e := c.ShouldBindJSON(&req); e != nil {
|
|
response.Error(c, 400, 43001, "参数格式错误", nil)
|
|
return
|
|
}
|
|
if e := h.Platform.SetRetention(c, req.Days); e != nil {
|
|
response.Error(c, 502, 43005, e.Error(), nil)
|
|
return
|
|
}
|
|
response.OK(c, map[string]int{"history_retention_days": req.Days})
|
|
}
|
|
func (h *Handler) exportConfig(kind string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Content-Type", "text/csv; charset=utf-8-sig")
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.csv"`, map[string]string{"device": "devices", "collection": "collection_points", "write": "write_points"}[kind]))
|
|
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
|
|
w := csv.NewWriter(c.Writer)
|
|
defer w.Flush()
|
|
if kind == "device" {
|
|
items, e := h.Platform.ListDevices(c, "")
|
|
if e != nil {
|
|
return
|
|
}
|
|
w.Write([]string{"name", "protocol_type", "host", "port", "connect_timeout", "reconnect_interval", "protocol_config", "enabled"})
|
|
for _, d := range items {
|
|
w.Write([]string{d.Name, d.ProtocolType, d.Host, strconv.Itoa(d.Port), strconv.Itoa(d.ConnectTimeout), strconv.Itoa(d.ReconnectInterval), string(d.ProtocolConfig), strings.ToUpper(strconv.FormatBool(d.Enabled))})
|
|
}
|
|
return
|
|
}
|
|
items, e := h.Platform.ListPoints(c, kind, "")
|
|
if e != nil {
|
|
return
|
|
}
|
|
if kind == "collection" {
|
|
w.Write([]string{"name", "group_name", "device_name", "address", "data_type", "unit", "collect_interval", "store_history", "history_interval", "enabled"})
|
|
for _, p := range items {
|
|
w.Write([]string{p.Name, p.GroupName, p.DeviceName, p.Address, p.DataType, stringValue(p.Unit), strconv.Itoa(p.CollectInterval), strings.ToUpper(strconv.FormatBool(p.StoreHistory)), strconv.Itoa(p.HistoryInterval), strings.ToUpper(strconv.FormatBool(p.Enabled))})
|
|
}
|
|
} else {
|
|
w.Write([]string{"name", "group_name", "device_name", "address", "data_type", "unit", "write_enabled"})
|
|
for _, p := range items {
|
|
w.Write([]string{p.Name, p.GroupName, p.DeviceName, p.Address, p.DataType, stringValue(p.Unit), strings.ToUpper(strconv.FormatBool(p.WriteEnabled))})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
func stringValue(v *string) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
return *v
|
|
}
|
|
func (h *Handler) importConfig(kind string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
file, e := c.FormFile("file")
|
|
if e != nil {
|
|
response.Error(c, 400, 10001, "缺少file字段", nil)
|
|
return
|
|
}
|
|
f, e := file.Open()
|
|
if e != nil {
|
|
serverError(c, e)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
rows, e := csv.NewReader(f).ReadAll()
|
|
if e != nil || len(rows) < 1 {
|
|
response.Error(c, 400, 10001, "CSV格式错误", nil)
|
|
return
|
|
}
|
|
headers := map[string]int{}
|
|
for i, v := range rows[0] {
|
|
headers[strings.TrimPrefix(v, "\ufeff")] = i
|
|
}
|
|
result := map[string]any{"total": len(rows) - 1, "created": 0, "updated": 0, "failed": 0, "errors": []map[string]any{}}
|
|
errs := []map[string]any{}
|
|
for i, row := range rows[1:] {
|
|
created, err := h.importRow(c, kind, headers, row)
|
|
if err != nil {
|
|
result["failed"] = result["failed"].(int) + 1
|
|
errs = append(errs, map[string]any{"row": i + 2, "field": "row", "message": err.Error()})
|
|
} else if created {
|
|
result["created"] = result["created"].(int) + 1
|
|
} else {
|
|
result["updated"] = result["updated"].(int) + 1
|
|
}
|
|
}
|
|
result["errors"] = errs
|
|
response.OK(c, result)
|
|
}
|
|
}
|
|
func (h *Handler) importRow(ctx context.Context, kind string, head map[string]int, row []string) (bool, error) {
|
|
get := func(k string) string {
|
|
i, ok := head[k]
|
|
if !ok || i >= len(row) {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(row[i])
|
|
}
|
|
if kind == "device" {
|
|
cfg := json.RawMessage(get("protocol_config"))
|
|
enabled, e := parseCSVBool(get("enabled"), true)
|
|
if e != nil {
|
|
return false, e
|
|
}
|
|
d := model.Device{Name: get("name"), ProtocolType: get("protocol_type"), Host: get("host"), Port: parseInt(get("port"), 0), ConnectTimeout: parseInt(get("connect_timeout"), 5), ReconnectInterval: parseInt(get("reconnect_interval"), 10), ProtocolConfig: cfg, Enabled: enabled}
|
|
existing, e := h.Platform.FindDeviceByName(ctx, d.Name)
|
|
created := platform.IsNotFound(e)
|
|
var id *uuid.UUID
|
|
if !created {
|
|
id = &existing.ID
|
|
}
|
|
return created, h.Platform.SaveDevice(ctx, id, &d)
|
|
}
|
|
device, e := h.Platform.FindDeviceByName(ctx, get("device_name"))
|
|
if e != nil {
|
|
return false, fmt.Errorf("device_name不存在")
|
|
}
|
|
existing, e := h.Platform.FindPointByName(ctx, kind, get("name"))
|
|
created := platform.IsNotFound(e)
|
|
enabled := true
|
|
if kind == "collection" {
|
|
var be error
|
|
enabled, be = parseCSVBool(get("enabled"), true)
|
|
if be != nil {
|
|
return false, be
|
|
}
|
|
}
|
|
unit := get("unit")
|
|
p := postgres.PointRow{ID: uuid.Nil, Name: get("name"), GroupName: get("group_name"), DeviceID: device.ID, Enabled: enabled, Address: get("address"), DataType: get("data_type"), Unit: &unit}
|
|
if !created {
|
|
p.ID = existing.ID
|
|
}
|
|
if kind == "collection" {
|
|
p.CollectInterval = parseInt(get("collect_interval"), 1)
|
|
p.StoreHistory, _ = parseCSVBool(get("store_history"), true)
|
|
p.HistoryInterval = parseInt(get("history_interval"), 1)
|
|
} else {
|
|
p.WriteEnabled, _ = parseCSVBool(get("write_enabled"), false)
|
|
}
|
|
return created, h.Platform.SavePoint(ctx, kind, &p)
|
|
}
|
|
func parseInt(v string, d int) int {
|
|
if v == "" {
|
|
return d
|
|
}
|
|
n, e := strconv.Atoi(v)
|
|
if e != nil {
|
|
return d
|
|
}
|
|
return n
|
|
}
|
|
func parseCSVBool(v string, d bool) (bool, error) {
|
|
if v == "" {
|
|
return d, nil
|
|
}
|
|
return strconv.ParseBool(strings.ToLower(v))
|
|
}
|
|
func (h *Handler) savePoint(kind string, update bool) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var p struct {
|
|
ID uuid.UUID `json:"-"`
|
|
Name string `json:"name"`
|
|
GroupName string `json:"group_name"`
|
|
DeviceID uuid.UUID `json:"device_id"`
|
|
Enabled *bool `json:"enabled"`
|
|
WriteEnabled bool `json:"write_enabled"`
|
|
Address string `json:"address"`
|
|
DataType string `json:"data_type"`
|
|
Unit *string `json:"unit"`
|
|
CollectInterval int `json:"collect_interval"`
|
|
StoreHistory *bool `json:"store_history"`
|
|
HistoryInterval int `json:"history_interval"`
|
|
}
|
|
if e := c.ShouldBindJSON(&p); e != nil {
|
|
response.Error(c, 400, 41001, "参数格式错误", nil)
|
|
return
|
|
}
|
|
id := uuid.Nil
|
|
if update {
|
|
var e error
|
|
id, e = uuid.Parse(c.Param("id"))
|
|
if e != nil {
|
|
response.Error(c, 400, 41001, "无效ID", nil)
|
|
return
|
|
}
|
|
}
|
|
enabled := true
|
|
if p.Enabled != nil {
|
|
enabled = *p.Enabled
|
|
}
|
|
storeHistory := true
|
|
if p.StoreHistory != nil {
|
|
storeHistory = *p.StoreHistory
|
|
}
|
|
if p.CollectInterval == 0 {
|
|
p.CollectInterval = 1
|
|
}
|
|
if p.HistoryInterval == 0 {
|
|
p.HistoryInterval = 1
|
|
}
|
|
if kind == "write" {
|
|
enabled = false
|
|
}
|
|
row := postgres.PointRow{ID: id, Name: p.Name, GroupName: p.GroupName, DeviceID: p.DeviceID, Enabled: enabled, WriteEnabled: p.WriteEnabled, Address: p.Address, DataType: p.DataType, Unit: p.Unit, CollectInterval: p.CollectInterval, StoreHistory: storeHistory, HistoryInterval: p.HistoryInterval, PointKind: kind}
|
|
if e := h.Platform.SavePoint(c, kind, &row); e != nil {
|
|
code := 41001
|
|
if kind == "write" {
|
|
code = 42001
|
|
}
|
|
response.Error(c, 400, code, e.Error(), nil)
|
|
return
|
|
}
|
|
if update {
|
|
response.OK(c, row)
|
|
} else {
|
|
response.Created(c, row)
|
|
}
|
|
}
|
|
}
|
|
func (h *Handler) deletePoint(kind string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, e := uuid.Parse(c.Param("id"))
|
|
if e != nil {
|
|
response.Error(c, 400, 41001, "无效ID", nil)
|
|
return
|
|
}
|
|
if e = h.Platform.DeletePoint(c, kind, id); e != nil {
|
|
response.Error(c, 404, 41004, "点位不存在", nil)
|
|
return
|
|
}
|
|
response.OK(c, nil)
|
|
}
|
|
}
|
|
func (h *Handler) writePoint(c *gin.Context) {
|
|
id, e := uuid.Parse(c.Param("id"))
|
|
if e != nil {
|
|
response.Error(c, 400, 42001, "无效ID", nil)
|
|
return
|
|
}
|
|
var req struct {
|
|
Value any `json:"value"`
|
|
Reason *string `json:"reason"`
|
|
Source any `json:"source"`
|
|
Operator any `json:"operator"`
|
|
}
|
|
if e = c.ShouldBindJSON(&req); e != nil || req.Source != nil || req.Operator != nil {
|
|
response.Error(c, 400, 42001, "参数格式错误,source/operator不允许由请求指定", nil)
|
|
return
|
|
}
|
|
data, code, e := h.Platform.ExecuteWrite(c, id, req.Value, req.Reason)
|
|
if e != nil {
|
|
status := http.StatusBadRequest
|
|
if code >= 51000 {
|
|
status = http.StatusBadGateway
|
|
}
|
|
response.Error(c, status, code, e.Error(), data)
|
|
return
|
|
}
|
|
response.OK(c, data)
|
|
}
|
|
func (h *Handler) logs(c *gin.Context) {
|
|
items, e := h.Platform.ListLogs(c)
|
|
if e != nil {
|
|
serverError(c, e)
|
|
return
|
|
}
|
|
filtered := make([]map[string]any, 0, len(items))
|
|
for _, item := range items {
|
|
if v := c.Query("point_id"); v != "" && fmt.Sprint(item["point_id"]) != v {
|
|
continue
|
|
}
|
|
if v := c.Query("device_id"); v != "" && fmt.Sprint(item["device_id"]) != v {
|
|
continue
|
|
}
|
|
if v := c.Query("result"); v != "" && fmt.Sprint(item["result"]) != v {
|
|
continue
|
|
}
|
|
if v := strings.ToLower(c.Query("keyword")); v != "" && !strings.Contains(strings.ToLower(fmt.Sprint(item["point_name"])), v) {
|
|
continue
|
|
}
|
|
filtered = append(filtered, item)
|
|
}
|
|
pageNo, pageSize := pagination(c)
|
|
start, end := bounds(len(filtered), pageNo, pageSize)
|
|
response.OK(c, map[string]any{"total": len(filtered), "page": pageNo, "page_size": pageSize, "items": filtered[start:end]})
|
|
}
|
|
func page(items any) map[string]any {
|
|
return map[string]any{"total": length(items), "page": 1, "page_size": 100, "items": items}
|
|
}
|
|
func length(v any) int {
|
|
switch x := v.(type) {
|
|
case []model.Device:
|
|
return len(x)
|
|
case []map[string]any:
|
|
return len(x)
|
|
default:
|
|
b, _ := json.Marshal(v)
|
|
var a []any
|
|
json.Unmarshal(b, &a)
|
|
return len(a)
|
|
}
|
|
}
|
|
func (h *Handler) tree(c *gin.Context) {
|
|
tree, e := h.History.Tree(c)
|
|
if e != nil {
|
|
serverError(c, e)
|
|
return
|
|
}
|
|
response.OK(c, map[string]any{"tree": tree})
|
|
}
|
|
|
|
func (h *Handler) cleanupHistoryArchive(c *gin.Context) {
|
|
count, e := h.History.CleanupDeletedArchives(c)
|
|
if e != nil {
|
|
serverError(c, e)
|
|
return
|
|
}
|
|
response.OK(c, map[string]int{"deleted_points": count})
|
|
}
|
|
|
|
type historyRequest struct {
|
|
PointIDs []uuid.UUID `json:"point_ids"`
|
|
StartTime time.Time `json:"start_time"`
|
|
EndTime time.Time `json:"end_time"`
|
|
MaxSamples int `json:"max_samples"`
|
|
IntervalMinutes int `json:"interval_minutes"`
|
|
}
|
|
|
|
func bindHistory(c *gin.Context) (historyRequest, bool) {
|
|
var r historyRequest
|
|
if e := c.ShouldBindJSON(&r); e != nil {
|
|
response.Error(c, 400, 43001, "参数格式错误", nil)
|
|
return r, false
|
|
}
|
|
if len(r.PointIDs) < 1 || len(r.PointIDs) > 20 {
|
|
response.Error(c, 400, 43002, "点位数量必须为1~20", nil)
|
|
return r, false
|
|
}
|
|
if !r.EndTime.After(r.StartTime) || r.EndTime.Sub(r.StartTime) > 31*24*time.Hour {
|
|
response.Error(c, 400, 43003, "时间范围无效或超过31天", nil)
|
|
return r, false
|
|
}
|
|
return r, true
|
|
}
|
|
func (h *Handler) historyQuery(c *gin.Context) {
|
|
r, ok := bindHistory(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if r.MaxSamples == 0 {
|
|
r.MaxSamples = 2000
|
|
}
|
|
if r.MaxSamples < 100 || r.MaxSamples > 10000 {
|
|
response.Error(c, 400, 43001, "max_samples必须为100~10000", nil)
|
|
return
|
|
}
|
|
data, e := h.History.Query(c, r.PointIDs, r.StartTime, r.EndTime, r.MaxSamples)
|
|
if e != nil {
|
|
historyError(c, e)
|
|
return
|
|
}
|
|
response.OK(c, map[string]any{"series": data})
|
|
}
|
|
func (h *Handler) historyTable(c *gin.Context) {
|
|
r, ok := bindHistory(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if r.IntervalMinutes < 1 || r.IntervalMinutes > 1440 {
|
|
response.Error(c, 400, 43001, "interval_minutes必须为1~1440", nil)
|
|
return
|
|
}
|
|
data, e := h.History.QueryTable(c, r.PointIDs, r.StartTime, r.EndTime, r.IntervalMinutes)
|
|
if e != nil {
|
|
historyError(c, e)
|
|
return
|
|
}
|
|
response.OK(c, data)
|
|
}
|
|
func (h *Handler) exportHistory(c *gin.Context) {
|
|
r, ok := bindHistory(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if r.IntervalMinutes < 1 || r.IntervalMinutes > 1440 {
|
|
response.Error(c, 400, 43001, "interval_minutes必须为1~1440", nil)
|
|
return
|
|
}
|
|
rows := int(r.EndTime.Sub(r.StartTime)/(time.Duration(r.IntervalMinutes)*time.Minute)) + 1
|
|
if rows > 50000 {
|
|
response.Error(c, 413, 43006, "导出行数超过50000", nil)
|
|
return
|
|
}
|
|
data, e := h.History.QueryTable(c, r.PointIDs, r.StartTime, r.EndTime, r.IntervalMinutes)
|
|
if e != nil {
|
|
historyError(c, e)
|
|
return
|
|
}
|
|
name := fmt.Sprintf("history_%s_%s_%dm.csv", r.StartTime.Format("20060102T150405"), r.EndTime.Format("20060102T150405"), r.IntervalMinutes)
|
|
c.Header("Content-Type", "text/csv; charset=utf-8-sig")
|
|
c.Header("Content-Disposition", `attachment; filename="`+name+`"`)
|
|
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
|
|
w := csv.NewWriter(c.Writer)
|
|
head := []string{"时间"}
|
|
used := map[string]bool{}
|
|
for _, col := range data.Columns {
|
|
n := col.PointName
|
|
if col.Unit != nil {
|
|
n += "[" + *col.Unit + "]"
|
|
}
|
|
if used[n] {
|
|
n += "_" + col.PointID.String()[:8]
|
|
}
|
|
used[n] = true
|
|
head = append(head, n)
|
|
}
|
|
w.Write(head)
|
|
for i, t := range data.TimeColumn {
|
|
row := []string{t.Format("2006-01-02 15:04:05")}
|
|
for _, col := range data.Columns {
|
|
x := col.Data[i]
|
|
if x.Value != nil {
|
|
row = append(row, strconv.FormatFloat(*x.Value, 'f', -1, 64))
|
|
} else {
|
|
row = append(row, "")
|
|
}
|
|
}
|
|
w.Write(row)
|
|
}
|
|
w.Flush()
|
|
}
|
|
func serverError(c *gin.Context, e error) {
|
|
slog.Error("服务执行失败", "error", e)
|
|
response.Error(c, 500, 10001, "服务内部错误", nil)
|
|
}
|
|
func historyError(c *gin.Context, e error) {
|
|
slog.Error("历史查询失败", "error", e)
|
|
code, status := 43005, 502
|
|
if strings.Contains(e.Error(), "元数据不存在") {
|
|
code, status = 43004, 404
|
|
}
|
|
response.Error(c, status, code, map[int]string{43004: "点位元数据不存在", 43005: "TDengine查询失败"}[code], nil)
|
|
}
|
|
|
|
var _ context.Context
|