safesight-control/internal/service/standard_templates.go
tian 24b9400767 feat: 离线部署包一体化,启动自动导入种子数据,人脸库构建流程修复
- package.sh: 打包包含 configs 种子数据、tools 构建工具、deps 离线依赖、scripts 辅助脚本
- ImportSeedAssets: 首次启动检测空库自动导入 templates/profiles/overlays
- face_gallery.html: 添加构建人脸库按钮
- device.html: 无标准配置时显示设备实际模型/资源,无人脸库配置时隐藏按钮
- register-face-gallery.py: 手动注册已构建人脸库到 standard_resources
- deployment.md: 更新部署流程反映新包结构
- .gitignore: 排除 *.tar.gz 构建产物
2026-07-23 17:57:47 +08:00

267 lines
9.1 KiB
Go

package service
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"safesight-control/internal/storage"
)
// ImportSeedAssets imports all config assets (templates, profiles, overlays) from a
// seed configs directory on first run. Returns total import count.
// The seed directory is expected to have subdirectories: templates/, profiles/, overlays/.
func ImportSeedAssets(repo *storage.AssetsRepo, seedDir string) (int, error) {
if repo == nil {
return 0, nil
}
seedDir = filepath.Clean(strings.TrimSpace(seedDir))
if seedDir == "" {
return 0, nil
}
if _, err := os.Stat(seedDir); os.IsNotExist(err) {
return 0, nil
}
// Check if DB already has assets (not first run).
existing, err := repo.ListTemplates()
if err != nil {
return 0, err
}
if len(existing) > 0 {
return 0, nil
}
total := 0
for _, kind := range []string{"templates", "profiles", "overlays"} {
dir := filepath.Join(seedDir, kind)
files, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
continue
}
return total, err
}
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(strings.ToLower(f.Name()), ".json") {
continue
}
path := filepath.Join(dir, f.Name())
body, err := os.ReadFile(path)
if err != nil {
return total, fmt.Errorf("%s: %w", path, err)
}
var raw map[string]any
if err := json.Unmarshal(body, &raw); err != nil {
return total, fmt.Errorf("%s: %w", path, err)
}
if raw == nil {
raw = map[string]any{}
}
name := strings.TrimSpace(firstString(raw["name"], strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))))
if name == "" {
log.Printf("skip %s: empty name", path)
continue
}
desc := stringValue(raw["description"])
switch kind {
case "templates":
if strings.TrimSpace(stringValue(raw["source"])) == "" {
raw["source"] = "standard"
}
b, _ := marshalConfigJSON(raw)
if err := repo.SaveTemplate(name, desc, string(b)); err != nil {
return total, fmt.Errorf("template %s: %w", name, err)
}
case "profiles":
tmplName := stringValue(raw["primary_template_name"])
bizName := stringValue(raw["business_name"])
b, _ := marshalConfigJSON(raw)
if err := repo.SaveProfile(name, tmplName, bizName, desc, string(b)); err != nil {
return total, fmt.Errorf("profile %s: %w", name, err)
}
case "overlays":
b, _ := marshalConfigJSON(raw)
if err := repo.SaveOverlay(name, desc, string(b)); err != nil {
return total, fmt.Errorf("overlay %s: %w", name, err)
}
}
total++
}
}
return total, nil
}
func ImportStandardTemplatesFromDir(repo *storage.AssetsRepo, dir string) (int, error) {
if repo == nil {
return 0, fmt.Errorf("asset repository is not configured")
}
dir = filepath.Clean(strings.TrimSpace(dir))
if dir == "" {
return 0, fmt.Errorf("standard template dir is empty")
}
files, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return 0, nil
}
return 0, err
}
imported := 0
for _, file := range files {
if file.IsDir() || strings.ToLower(filepath.Ext(file.Name())) != ".json" {
continue
}
path := filepath.Join(dir, file.Name())
body, err := os.ReadFile(path)
if err != nil {
return imported, err
}
var raw map[string]any
if err := json.Unmarshal(body, &raw); err != nil {
return imported, fmt.Errorf("%s: %w", path, err)
}
if raw == nil {
raw = map[string]any{}
}
name := strings.TrimSpace(firstString(raw["name"], strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))))
if name == "" {
return imported, fmt.Errorf("%s: template name is empty", path)
}
if err := validateConfigName(name); err != nil {
return imported, fmt.Errorf("%s: invalid template name %q: %w", path, name, err)
}
if !isStandardTemplateName(name) {
return imported, fmt.Errorf("%s: standard template name must start with std_", path)
}
if strings.TrimSpace(stringValue(raw["source"])) == "" {
raw["source"] = "standard"
}
body, err = marshalConfigJSON(raw)
if err != nil {
return imported, err
}
existing, err := repo.GetTemplate(name)
if err != nil {
return imported, err
}
if existing != nil &&
strings.TrimSpace(existing.Description) == strings.TrimSpace(stringValue(raw["description"])) &&
strings.TrimSpace(existing.BodyJSON) == strings.TrimSpace(string(body)) {
continue
}
if err := repo.SaveTemplate(name, stringValue(raw["description"]), string(body)); err != nil {
return imported, err
}
imported++
}
return imported, nil
}
func ImportStandardOverlaysFromDir(repo *storage.AssetsRepo, dir string) (int, error) {
if repo == nil {
return 0, fmt.Errorf("asset repository is not configured")
}
dir = filepath.Clean(strings.TrimSpace(dir))
if dir == "" {
return 0, fmt.Errorf("standard overlay dir is empty")
}
files, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return 0, nil
}
return 0, err
}
imported := 0
for _, file := range files {
if file.IsDir() || strings.ToLower(filepath.Ext(file.Name())) != ".json" {
continue
}
path := filepath.Join(dir, file.Name())
body, err := os.ReadFile(path)
if err != nil {
return imported, err
}
var raw map[string]any
if err := json.Unmarshal(body, &raw); err != nil {
return imported, fmt.Errorf("%s: %w", path, err)
}
if raw == nil {
raw = map[string]any{}
}
name := strings.TrimSpace(firstString(raw["name"], strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))))
if name == "" {
return imported, fmt.Errorf("%s: overlay name is empty", path)
}
if err := validateConfigName(name); err != nil {
return imported, fmt.Errorf("%s: invalid overlay name %q: %w", path, name, err)
}
if !isStandardTemplateName(name) {
return imported, fmt.Errorf("%s: standard overlay name must start with std_", path)
}
description := strings.TrimSpace(stringValue(raw["description"]))
delete(raw, "name")
delete(raw, "description")
body, err = marshalConfigJSON(raw)
if err != nil {
return imported, err
}
existing, err := repo.GetOverlay(name)
if err != nil {
return imported, err
}
if existing != nil &&
strings.TrimSpace(existing.Description) == description &&
strings.TrimSpace(existing.BodyJSON) == strings.TrimSpace(string(body)) {
continue
}
if err := repo.SaveOverlay(name, description, string(body)); err != nil {
return imported, err
}
imported++
}
return imported, nil
}
// EnsureDefaultIntegrations creates the default alarm and storage integration
// services if they don't already exist. Call during startup.
func EnsureDefaultIntegrations(repo *storage.AssetsRepo) error {
defaults := []struct {
Name string
ServiceType string
Description string
Enabled bool
BodyJSON string
}{
{
Name: "alarm",
ServiceType: "alarm_service",
Description: "告警推送 HTTP API",
Enabled: true,
BodyJSON: `{"get_token_url":"http://10.0.0.49:8080/api/getToken","put_message_url":"http://10.0.0.49:8080/api/putMessage","tenant_code":"32"}`,
},
{
Name: "storage",
ServiceType: "object_storage",
Description: "告警截图和视频存储",
Enabled: true,
BodyJSON: `{"endpoint":"http://10.0.0.49:9000","bucket":"myminio","access_key":"admin","secret_key":"password"}`,
},
}
for _, item := range defaults {
existing, _ := repo.GetIntegrationService(item.Name)
if existing != nil {
continue
}
if err := repo.SaveIntegrationService(item.Name, item.ServiceType, item.Description, item.Enabled, item.BodyJSON); err != nil {
return err
}
}
return nil
}