fix: persist features only after pipeline success, not before

actionConsoleSave: 先跑流水线,成功后再存 DB。
空功能提交不再清空 DB,改为提示用户。
This commit is contained in:
tian 2026-05-12 15:16:49 +08:00
parent 67e8c3617a
commit 418bf3c80b
2 changed files with 23 additions and 13 deletions

View File

@ -1128,30 +1128,26 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) {
u.ensureDevicesLoaded()
devices := u.registry.GetDevices()
// Step 1: persist feature selections to DB (source of truth).
// Step 1: collect form features per device.
formFeatures := map[string][]string{}
for _, dev := range devices {
if dev == nil || dev.DeviceID == "" {
continue
}
features := cleanFormList(r.Form["device_"+dev.DeviceID+"_feature"])
if err := u.preview.SaveDeviceFeatures(dev.DeviceID, features); err != nil {
http.Redirect(w, r, "/ui/console?error="+url.QueryEscape("保存功能配置失败: "+err.Error()), http.StatusFound)
return
if len(features) > 0 {
formFeatures[dev.DeviceID] = features
}
}
// Step 2: build requests from DB only.
// Features come from device_features table.
// Video sources come from existing device assignments and recognition units.
// Step 2: build requests using form features (user intent) and
// existing recognition unit video sources.
var requests []service.AutoConfigRequest
for _, dev := range devices {
if dev == nil || dev.DeviceID == "" {
continue
}
features, err := u.preview.GetDeviceFeatures(dev.DeviceID)
if err != nil {
continue
}
features := formFeatures[dev.DeviceID]
var sources []string
if assignment, err := u.preview.GetDeviceAssignment(dev.DeviceID); err == nil && assignment != nil {
for _, ref := range assignment.RecognitionUnits {
@ -1162,7 +1158,7 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) {
}
}
}
if len(features) == 0 && len(sources) == 0 {
if len(features) == 0 {
continue
}
requests = append(requests, service.AutoConfigRequest{
@ -1173,12 +1169,22 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) {
}
if len(requests) == 0 {
http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape("未选择检测功能或未分配视频源"), http.StatusFound)
http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape("未选择检测功能"), http.StatusFound)
return
}
// Step 3: run pipeline. Only persist to DB on success.
results, _ := u.autoConfig.BuildPipelineBatch(requests, true)
// Step 4: persist successful feature selections to DB.
for _, res := range results {
if res.Error == "" && res.TaskID != "" {
if feats, ok := formFeatures[res.DeviceID]; ok {
_ = u.preview.SaveDeviceFeatures(res.DeviceID, feats)
}
}
}
// Collect task IDs for redirect.
taskIDs := make([]string, 0)
var errs []string

View File

@ -356,6 +356,10 @@
});
});
if (!summary) { alert('未选择任何检测功能或视频源'); return; }
// Require at least one feature.
var hasFeature = false;
document.querySelectorAll('.console-device-row input[type=checkbox]:checked').forEach(function() { hasFeature = true; });
if (!hasFeature) { alert('请至少选择一项检测功能'); return; }
var overlay = document.createElement('div');
overlay.className = 'confirm-overlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center';