safesight-control/internal/service/standard_templates.go
tian ce18c85eba feat: 标准调试参数 & 克隆不立即保存
1. 新增标准调试参数
   - templates/standard_overlays/ 目录下导入 std_face_debug、
     std_test_sensitive、std_production_quiet 三个标准调试参数
   - 启动时通过 ImportStandardOverlaysFromDir 自动导入 SQLite
   - 支持 std_ 前缀只读保护,不可直接编辑/删除

2. 复制后不立即保存
   - 调试参数克隆:重定向到新建页预填数据,点击保存才入库
   - 识别模板克隆:重定向到模板编辑页预填,点击创建/进入可视化编辑才入库
   - 可视化编辑也支持 clone_source 参数,编辑保存时创建新模板

3. 简化调试参数 JSON 结构
   - 去掉 instance_overrides 层级,改为顶层 override 键
   - 运行时渲染兼容新旧两种格式
   - UI 去掉目标/目标数量字段

4. 清理死代码
   - 移除从零新建空白调试参数/模板的代码路径
2026-05-09 11:39:02 +08:00

144 lines
4.7 KiB
Go

package service
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"3588AdminBackend/internal/storage"
)
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
}