- device_features 表:持久化设备检测功能选择 - pageConsole 从 DB 读取功能选择(非从模板反推) - actionConsoleSave 先存 DB 再从 DB 读取运行流水线 - 模板选择改为最精确匹配(单功能只用该功能模板) - 修复内存指标解析(补充 memory 字段)
801 lines
25 KiB
Go
801 lines
25 KiB
Go
package storage
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type AssetRecord struct {
|
|
Name string
|
|
Description string
|
|
TemplateName string
|
|
BusinessName string
|
|
BodyJSON string
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
}
|
|
|
|
type IntegrationServiceRecord struct {
|
|
Name string
|
|
ServiceType string
|
|
Description string
|
|
Enabled bool
|
|
BodyJSON string
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
}
|
|
|
|
type VideoSourceRecord struct {
|
|
Name string
|
|
SourceType string
|
|
Area string
|
|
Description string
|
|
BodyJSON string
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
}
|
|
|
|
type DeviceAssignmentRecord struct {
|
|
DeviceID string
|
|
ProfileName string
|
|
Description string
|
|
BodyJSON string
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
}
|
|
|
|
type RecognitionUnitRecord struct {
|
|
SceneTemplateName string
|
|
Name string
|
|
DisplayName string
|
|
SiteName string
|
|
VideoSourceRef string
|
|
OutputChannel string
|
|
RTSPPort string
|
|
Description string
|
|
BodyJSON string
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
}
|
|
|
|
type AssetsRepo struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewAssetsRepo(db *sql.DB) *AssetsRepo {
|
|
return &AssetsRepo{db: db}
|
|
}
|
|
|
|
func (r *AssetsRepo) SaveTemplate(name string, description string, bodyJSON string) error {
|
|
return r.saveAsset("templates", AssetRecord{
|
|
Name: name,
|
|
Description: description,
|
|
BodyJSON: bodyJSON,
|
|
})
|
|
}
|
|
|
|
func (r *AssetsRepo) SaveProfile(name string, templateName string, businessName string, description string, bodyJSON string) error {
|
|
return r.saveAsset("profiles", AssetRecord{
|
|
Name: name,
|
|
TemplateName: templateName,
|
|
BusinessName: businessName,
|
|
Description: description,
|
|
BodyJSON: bodyJSON,
|
|
})
|
|
}
|
|
|
|
func (r *AssetsRepo) SaveOverlay(name string, description string, bodyJSON string) error {
|
|
return r.saveAsset("overlays", AssetRecord{
|
|
Name: name,
|
|
Description: description,
|
|
BodyJSON: bodyJSON,
|
|
})
|
|
}
|
|
|
|
func (r *AssetsRepo) SaveIntegrationService(name string, serviceType string, description string, enabled bool, bodyJSON string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
now := time.Now().Format(time.RFC3339)
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO integration_services(name, type, description, enabled, body_json, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM integration_services WHERE name = ?), ?), ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
type=excluded.type,
|
|
description=excluded.description,
|
|
enabled=excluded.enabled,
|
|
body_json=excluded.body_json,
|
|
updated_at=excluded.updated_at
|
|
`, name, serviceType, description, enabled, bodyJSON, name, now, now)
|
|
return err
|
|
}
|
|
|
|
func (r *AssetsRepo) SaveVideoSource(name string, sourceType string, area string, description string, bodyJSON string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
now := time.Now().Format(time.RFC3339)
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO video_sources(name, source_type, area, description, body_json, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM video_sources WHERE name = ?), ?), ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
source_type=excluded.source_type,
|
|
area=excluded.area,
|
|
description=excluded.description,
|
|
body_json=excluded.body_json,
|
|
updated_at=excluded.updated_at
|
|
`, name, sourceType, area, description, bodyJSON, name, now, now)
|
|
return err
|
|
}
|
|
|
|
func (r *AssetsRepo) SaveDeviceAssignment(deviceID string, profileName string, description string, bodyJSON string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
now := time.Now().Format(time.RFC3339)
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO device_assignments(device_id, profile_name, description, body_json, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, COALESCE((SELECT created_at FROM device_assignments WHERE device_id = ?), ?), ?)
|
|
ON CONFLICT(device_id) DO UPDATE SET
|
|
profile_name=excluded.profile_name,
|
|
description=excluded.description,
|
|
body_json=excluded.body_json,
|
|
updated_at=excluded.updated_at
|
|
`, deviceID, profileName, description, bodyJSON, deviceID, now, now)
|
|
return err
|
|
}
|
|
|
|
func (r *AssetsRepo) SaveRecognitionUnit(record RecognitionUnitRecord) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
now := time.Now().Format(time.RFC3339)
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO recognition_units(scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM recognition_units WHERE scene_template_name = ? AND name = ?), ?), ?)
|
|
ON CONFLICT(scene_template_name, name) DO UPDATE SET
|
|
display_name=excluded.display_name,
|
|
site_name=excluded.site_name,
|
|
video_source_ref=excluded.video_source_ref,
|
|
output_channel=excluded.output_channel,
|
|
rtsp_port=excluded.rtsp_port,
|
|
description=excluded.description,
|
|
body_json=excluded.body_json,
|
|
updated_at=excluded.updated_at
|
|
`, record.SceneTemplateName, record.Name, record.DisplayName, record.SiteName, record.VideoSourceRef, record.OutputChannel, record.RTSPPort, record.Description, record.BodyJSON, record.SceneTemplateName, record.Name, now, now)
|
|
return err
|
|
}
|
|
|
|
func (r *AssetsRepo) ListTemplates() ([]AssetRecord, error) {
|
|
return r.listAssets("templates")
|
|
}
|
|
|
|
func (r *AssetsRepo) ListProfiles() ([]AssetRecord, error) {
|
|
return r.listAssets("profiles")
|
|
}
|
|
|
|
func (r *AssetsRepo) ListOverlays() ([]AssetRecord, error) {
|
|
return r.listAssets("overlays")
|
|
}
|
|
|
|
func (r *AssetsRepo) ListIntegrationServices() ([]IntegrationServiceRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
rows, err := r.db.Query(`
|
|
SELECT name, type, description, enabled, body_json, created_at, updated_at
|
|
FROM integration_services
|
|
ORDER BY updated_at DESC, name ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []IntegrationServiceRecord
|
|
for rows.Next() {
|
|
var item IntegrationServiceRecord
|
|
if err := rows.Scan(&item.Name, &item.ServiceType, &item.Description, &item.Enabled, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (r *AssetsRepo) ListVideoSources() ([]VideoSourceRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
rows, err := r.db.Query(`
|
|
SELECT name, source_type, area, description, body_json, created_at, updated_at
|
|
FROM video_sources
|
|
ORDER BY updated_at DESC, name ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []VideoSourceRecord
|
|
for rows.Next() {
|
|
var item VideoSourceRecord
|
|
if err := rows.Scan(&item.Name, &item.SourceType, &item.Area, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (r *AssetsRepo) ListDeviceAssignments() ([]DeviceAssignmentRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
rows, err := r.db.Query(`
|
|
SELECT device_id, profile_name, description, body_json, created_at, updated_at
|
|
FROM device_assignments
|
|
ORDER BY updated_at DESC, device_id ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []DeviceAssignmentRecord
|
|
for rows.Next() {
|
|
var item DeviceAssignmentRecord
|
|
if err := rows.Scan(&item.DeviceID, &item.ProfileName, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (r *AssetsRepo) ListRecognitionUnits() ([]RecognitionUnitRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
rows, err := r.db.Query(`
|
|
SELECT scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at
|
|
FROM recognition_units
|
|
ORDER BY scene_template_name ASC, name ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []RecognitionUnitRecord
|
|
for rows.Next() {
|
|
var item RecognitionUnitRecord
|
|
if err := rows.Scan(&item.SceneTemplateName, &item.Name, &item.DisplayName, &item.SiteName, &item.VideoSourceRef, &item.OutputChannel, &item.RTSPPort, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (r *AssetsRepo) GetTemplate(name string) (*AssetRecord, error) {
|
|
return r.getAsset("templates", name)
|
|
}
|
|
|
|
func (r *AssetsRepo) GetProfile(name string) (*AssetRecord, error) {
|
|
return r.getAsset("profiles", name)
|
|
}
|
|
|
|
func (r *AssetsRepo) GetOverlay(name string) (*AssetRecord, error) {
|
|
return r.getAsset("overlays", name)
|
|
}
|
|
|
|
func (r *AssetsRepo) GetIntegrationService(name string) (*IntegrationServiceRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
var item IntegrationServiceRecord
|
|
err := r.db.QueryRow(`
|
|
SELECT name, type, description, enabled, body_json, created_at, updated_at
|
|
FROM integration_services
|
|
WHERE name = ?
|
|
`, name).Scan(&item.Name, &item.ServiceType, &item.Description, &item.Enabled, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func (r *AssetsRepo) GetVideoSource(name string) (*VideoSourceRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
var item VideoSourceRecord
|
|
err := r.db.QueryRow(`
|
|
SELECT name, source_type, area, description, body_json, created_at, updated_at
|
|
FROM video_sources
|
|
WHERE name = ?
|
|
`, name).Scan(&item.Name, &item.SourceType, &item.Area, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func (r *AssetsRepo) GetDeviceAssignment(deviceID string) (*DeviceAssignmentRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
var item DeviceAssignmentRecord
|
|
err := r.db.QueryRow(`
|
|
SELECT device_id, profile_name, description, body_json, created_at, updated_at
|
|
FROM device_assignments
|
|
WHERE device_id = ?
|
|
`, deviceID).Scan(&item.DeviceID, &item.ProfileName, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func (r *AssetsRepo) GetRecognitionUnit(sceneTemplateName string, name string) (*RecognitionUnitRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
var item RecognitionUnitRecord
|
|
err := r.db.QueryRow(`
|
|
SELECT scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at
|
|
FROM recognition_units
|
|
WHERE scene_template_name = ? AND name = ?
|
|
`, sceneTemplateName, name).Scan(&item.SceneTemplateName, &item.Name, &item.DisplayName, &item.SiteName, &item.VideoSourceRef, &item.OutputChannel, &item.RTSPPort, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func (r *AssetsRepo) DeleteTemplate(name string) error {
|
|
return r.deleteAsset("templates", name)
|
|
}
|
|
|
|
func (r *AssetsRepo) DeleteProfile(name string) error {
|
|
return r.deleteAsset("profiles", name)
|
|
}
|
|
|
|
func (r *AssetsRepo) DeleteIntegrationService(name string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
_, err := r.db.Exec(`DELETE FROM integration_services WHERE name = ?`, name)
|
|
return err
|
|
}
|
|
|
|
func (r *AssetsRepo) DeleteVideoSource(name string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
_, err := r.db.Exec(`DELETE FROM video_sources WHERE name = ?`, name)
|
|
return err
|
|
}
|
|
|
|
func (r *AssetsRepo) DeleteDeviceAssignment(deviceID string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
_, err := r.db.Exec(`DELETE FROM device_assignments WHERE device_id = ?`, deviceID)
|
|
return err
|
|
}
|
|
|
|
func (r *AssetsRepo) DeleteRecognitionUnit(sceneTemplateName string, name string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
_, err := r.db.Exec(`DELETE FROM recognition_units WHERE scene_template_name = ? AND name = ?`, sceneTemplateName, name)
|
|
return err
|
|
}
|
|
|
|
func (r *AssetsRepo) DeleteOverlay(name string) error {
|
|
return r.deleteAsset("overlays", name)
|
|
}
|
|
|
|
func (r *AssetsRepo) RenameTemplate(oldName string, newName string, description string, bodyJSON string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
oldName = strings.TrimSpace(oldName)
|
|
newName = strings.TrimSpace(newName)
|
|
if oldName == "" || newName == "" {
|
|
return fmt.Errorf("template name is required")
|
|
}
|
|
if oldName == newName {
|
|
return r.SaveTemplate(newName, description, bodyJSON)
|
|
}
|
|
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if tx != nil {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
var exists int
|
|
if err := tx.QueryRow(`SELECT COUNT(1) FROM templates WHERE name = ?`, oldName).Scan(&exists); err != nil {
|
|
return err
|
|
}
|
|
if exists == 0 {
|
|
return sql.ErrNoRows
|
|
}
|
|
if err := tx.QueryRow(`SELECT COUNT(1) FROM templates WHERE name = ?`, newName).Scan(&exists); err != nil {
|
|
return err
|
|
}
|
|
if exists > 0 {
|
|
return fmt.Errorf("template %q already exists", newName)
|
|
}
|
|
|
|
now := time.Now().Format(time.RFC3339)
|
|
if _, err := tx.Exec(`
|
|
INSERT INTO templates(name, description, body_json, created_at, updated_at)
|
|
SELECT ?, ?, ?, created_at, ?
|
|
FROM templates
|
|
WHERE name = ?
|
|
`, newName, description, bodyJSON, now, oldName); err != nil {
|
|
return err
|
|
}
|
|
|
|
rows, err := tx.Query(`
|
|
SELECT name, description, business_name, body_json
|
|
FROM scene_templates
|
|
WHERE primary_template_name = ? OR body_json LIKE ?
|
|
`, oldName, "%\"primary_template_name\": \""+oldName+"\"%")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
type profileUpdate struct {
|
|
name string
|
|
description string
|
|
businessName string
|
|
bodyJSON string
|
|
}
|
|
updates := make([]profileUpdate, 0)
|
|
for rows.Next() {
|
|
var item profileUpdate
|
|
if err := rows.Scan(&item.name, &item.description, &item.businessName, &item.bodyJSON); err != nil {
|
|
return err
|
|
}
|
|
rewritten, changed, err := rewriteProfileTemplateRefs(item.bodyJSON, oldName, newName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed {
|
|
item.bodyJSON = rewritten
|
|
}
|
|
updates = append(updates, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
for _, item := range updates {
|
|
if _, err := tx.Exec(`
|
|
UPDATE scene_templates
|
|
SET primary_template_name = ?, body_json = ?, updated_at = ?
|
|
WHERE name = ?
|
|
`, newName, item.bodyJSON, now, item.name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := tx.Exec(`
|
|
UPDATE recognition_units
|
|
SET body_json = REPLACE(body_json, ?, ?), updated_at = ?
|
|
WHERE body_json LIKE ?
|
|
`, `"`+"template"+`": "`+oldName+`"`, `"`+"template"+`": "`+newName+`"`, now, "%\"template\": \""+oldName+"\"%"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.Exec(`DELETE FROM templates WHERE name = ?`, oldName); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
tx = nil
|
|
return nil
|
|
}
|
|
|
|
func (r *AssetsRepo) saveAsset(table string, record AssetRecord) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
now := time.Now().Format(time.RFC3339)
|
|
switch table {
|
|
case "templates", "overlays":
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO `+table+`(name, description, body_json, created_at, updated_at)
|
|
VALUES(?, ?, ?, COALESCE((SELECT created_at FROM `+table+` WHERE name = ?), ?), ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
description=excluded.description,
|
|
body_json=excluded.body_json,
|
|
updated_at=excluded.updated_at
|
|
`, record.Name, record.Description, record.BodyJSON, record.Name, now, now)
|
|
return err
|
|
case "profiles":
|
|
normalized, units, replaceUnits, err := normalizeProfileRecord(record)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
_, err = tx.Exec(`
|
|
INSERT INTO scene_templates(name, primary_template_name, business_name, description, body_json, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM scene_templates WHERE name = ?), ?), ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
primary_template_name=excluded.primary_template_name,
|
|
business_name=excluded.business_name,
|
|
description=excluded.description,
|
|
body_json=excluded.body_json,
|
|
updated_at=excluded.updated_at
|
|
`, normalized.Name, normalized.TemplateName, normalized.BusinessName, normalized.Description, normalized.BodyJSON, normalized.Name, now, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if replaceUnits {
|
|
if _, err := tx.Exec(`DELETE FROM recognition_units WHERE scene_template_name = ?`, normalized.Name); err != nil {
|
|
return err
|
|
}
|
|
for _, unit := range units {
|
|
if _, err := tx.Exec(`
|
|
INSERT INTO recognition_units(scene_template_name, name, display_name, site_name, video_source_ref, output_channel, rtsp_port, description, body_json, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM recognition_units WHERE scene_template_name = ? AND name = ?), ?), ?)
|
|
ON CONFLICT(scene_template_name, name) DO UPDATE SET
|
|
display_name=excluded.display_name,
|
|
site_name=excluded.site_name,
|
|
video_source_ref=excluded.video_source_ref,
|
|
output_channel=excluded.output_channel,
|
|
rtsp_port=excluded.rtsp_port,
|
|
description=excluded.description,
|
|
body_json=excluded.body_json,
|
|
updated_at=excluded.updated_at
|
|
`, unit.SceneTemplateName, unit.Name, unit.DisplayName, unit.SiteName, unit.VideoSourceRef, unit.OutputChannel, unit.RTSPPort, unit.Description, unit.BodyJSON, unit.SceneTemplateName, unit.Name, now, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func normalizeProfileRecord(record AssetRecord) (AssetRecord, []RecognitionUnitRecord, bool, error) {
|
|
raw := map[string]any{}
|
|
if strings.TrimSpace(record.BodyJSON) != "" {
|
|
if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil {
|
|
return AssetRecord{}, nil, false, err
|
|
}
|
|
}
|
|
if raw == nil {
|
|
raw = map[string]any{}
|
|
}
|
|
_, hasInstances := raw["instances"]
|
|
if !hasInstances {
|
|
return record, nil, false, nil
|
|
}
|
|
templateDoc, units, err := splitLegacyProfileDocument(struct {
|
|
Name string
|
|
TemplateName string
|
|
BusinessName string
|
|
Description string
|
|
BodyJSON string
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
}{
|
|
Name: record.Name,
|
|
TemplateName: record.TemplateName,
|
|
BusinessName: record.BusinessName,
|
|
Description: record.Description,
|
|
BodyJSON: record.BodyJSON,
|
|
CreatedAt: record.CreatedAt,
|
|
UpdatedAt: record.UpdatedAt,
|
|
})
|
|
if err != nil {
|
|
return AssetRecord{}, nil, false, err
|
|
}
|
|
body, err := json.MarshalIndent(templateDoc, "", " ")
|
|
if err != nil {
|
|
return AssetRecord{}, nil, false, err
|
|
}
|
|
normalized := record
|
|
normalized.BodyJSON = string(append(body, '\n'))
|
|
outUnits := make([]RecognitionUnitRecord, 0, len(units))
|
|
for _, unit := range units {
|
|
outUnits = append(outUnits, RecognitionUnitRecord{
|
|
SceneTemplateName: record.Name,
|
|
Name: unit.Name,
|
|
DisplayName: unit.DisplayName,
|
|
SiteName: unit.SiteName,
|
|
VideoSourceRef: unit.VideoSourceRef,
|
|
OutputChannel: unit.OutputChannel,
|
|
RTSPPort: unit.RTSPPort,
|
|
Description: record.Description,
|
|
BodyJSON: unit.BodyJSON,
|
|
CreatedAt: record.CreatedAt,
|
|
UpdatedAt: record.UpdatedAt,
|
|
})
|
|
}
|
|
return normalized, outUnits, true, nil
|
|
}
|
|
|
|
func (r *AssetsRepo) listAssets(table string) ([]AssetRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
query := `
|
|
SELECT name, description, body_json, created_at, updated_at, '', ''
|
|
FROM ` + table + `
|
|
ORDER BY updated_at DESC, name ASC
|
|
`
|
|
if table == "profiles" {
|
|
query = `
|
|
SELECT name, description, body_json, created_at, updated_at, primary_template_name, business_name
|
|
FROM scene_templates
|
|
ORDER BY updated_at DESC, name ASC
|
|
`
|
|
}
|
|
rows, err := r.db.Query(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []AssetRecord
|
|
for rows.Next() {
|
|
var item AssetRecord
|
|
if err := rows.Scan(&item.Name, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt, &item.TemplateName, &item.BusinessName); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (r *AssetsRepo) getAsset(table string, name string) (*AssetRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
query := `
|
|
SELECT name, description, body_json, created_at, updated_at, '', ''
|
|
FROM ` + table + `
|
|
WHERE name = ?
|
|
`
|
|
if table == "profiles" {
|
|
query = `
|
|
SELECT name, description, body_json, created_at, updated_at, primary_template_name, business_name
|
|
FROM scene_templates
|
|
WHERE name = ?
|
|
`
|
|
}
|
|
|
|
var item AssetRecord
|
|
err := r.db.QueryRow(query, name).Scan(&item.Name, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt, &item.TemplateName, &item.BusinessName)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func (r *AssetsRepo) deleteAsset(table string, name string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
if table == "profiles" {
|
|
table = "scene_templates"
|
|
}
|
|
_, err := r.db.Exec(`DELETE FROM `+table+` WHERE name = ?`, name)
|
|
return err
|
|
}
|
|
|
|
func rewriteProfileTemplateRefs(bodyJSON string, oldName string, newName string) (string, bool, error) {
|
|
var raw map[string]any
|
|
if err := json.Unmarshal([]byte(bodyJSON), &raw); err != nil {
|
|
return "", false, err
|
|
}
|
|
changed := false
|
|
if strings.TrimSpace(valueString(raw["primary_template_name"])) == oldName {
|
|
raw["primary_template_name"] = newName
|
|
changed = true
|
|
}
|
|
instances, _ := raw["instances"].([]any)
|
|
for _, item := range instances {
|
|
instanceMap, _ := item.(map[string]any)
|
|
if strings.TrimSpace(valueString(instanceMap["template"])) == oldName {
|
|
instanceMap["template"] = newName
|
|
changed = true
|
|
}
|
|
}
|
|
if !changed {
|
|
return bodyJSON, false, nil
|
|
}
|
|
body, err := json.MarshalIndent(raw, "", " ")
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
return string(append(body, '\n')), true, nil
|
|
}
|
|
|
|
func valueString(v any) string {
|
|
switch vv := v.(type) {
|
|
case string:
|
|
return vv
|
|
case float64:
|
|
return fmt.Sprintf("%v", vv)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// ---- device_features ----
|
|
|
|
// DeviceFeatureRecord holds the persisted feature selection for a device.
|
|
type DeviceFeatureRecord struct {
|
|
DeviceID string `json:"device_id"`
|
|
FeaturesJSON string `json:"features_json"` // JSON array of feature keys, e.g. ["face","shoe"]
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// SaveDeviceFeatures upserts the feature selection for a device.
|
|
func (r *AssetsRepo) SaveDeviceFeatures(deviceID string, featuresJSON string) error {
|
|
if r == nil || r.db == nil {
|
|
return nil
|
|
}
|
|
now := time.Now().Format(time.RFC3339)
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO device_features(device_id, features_json, updated_at)
|
|
VALUES(?, ?, ?)
|
|
ON CONFLICT(device_id) DO UPDATE SET
|
|
features_json=excluded.features_json,
|
|
updated_at=excluded.updated_at
|
|
`, deviceID, featuresJSON, now)
|
|
return err
|
|
}
|
|
|
|
// GetDeviceFeatures returns the persisted features for a device, or nil.
|
|
func (r *AssetsRepo) GetDeviceFeatures(deviceID string) (*DeviceFeatureRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, nil
|
|
}
|
|
var rec DeviceFeatureRecord
|
|
err := r.db.QueryRow(
|
|
"SELECT device_id, features_json, updated_at FROM device_features WHERE device_id = ?",
|
|
deviceID,
|
|
).Scan(&rec.DeviceID, &rec.FeaturesJSON, &rec.UpdatedAt)
|
|
if err != nil {
|
|
return nil, nil // not found is not an error
|
|
}
|
|
return &rec, nil
|
|
}
|