diff --git a/internal/storage/face_gallery_repo.go b/internal/storage/face_gallery_repo.go new file mode 100644 index 0000000..85faf43 --- /dev/null +++ b/internal/storage/face_gallery_repo.go @@ -0,0 +1,115 @@ +package storage + +import ( + "database/sql" + "fmt" + "time" +) + +type PersonRecord struct { + ID int `json:"id"` + Name string `json:"name"` + PhotoURL string `json:"photo_url,omitempty"` + CreatedAt string `json:"created_at"` +} + +type FaceGalleryRepo struct { + dbPath string +} + +func NewFaceGalleryRepo(dbPath string) *FaceGalleryRepo { + return &FaceGalleryRepo{dbPath: dbPath} +} + +func (r *FaceGalleryRepo) open() (*sql.DB, error) { + db, err := sql.Open("sqlite", r.dbPath) + if err != nil { + return nil, fmt.Errorf("open face gallery: %w", err) + } + // Ensure schema + _, err = db.Exec(` +CREATE TABLE IF NOT EXISTS person ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT '', + extra TEXT NOT NULL DEFAULT '' +); +CREATE TABLE IF NOT EXISTS embedding ( + id INTEGER PRIMARY KEY, + person_id INTEGER NOT NULL, + embedding BLOB NOT NULL, + created_at TEXT NOT NULL DEFAULT '', + FOREIGN KEY (person_id) REFERENCES person(id) +); +`) + if err != nil { + db.Close() + return nil, fmt.Errorf("ensure schema: %w", err) + } + return db, nil +} + +func (r *FaceGalleryRepo) ListPersons() ([]PersonRecord, error) { + db, err := r.open() + if err != nil { + return nil, err + } + defer db.Close() + + rows, err := db.Query(`SELECT p.id, p.name, p.created_at, COUNT(e.id) AS photo_count +FROM person p LEFT JOIN embedding e ON e.person_id = p.id +GROUP BY p.id ORDER BY p.name`) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []PersonRecord + for rows.Next() { + var p PersonRecord + var photoCount int + if err := rows.Scan(&p.ID, &p.Name, &p.CreatedAt, &photoCount); err != nil { + continue + } + out = append(out, p) + } + if out == nil { + out = make([]PersonRecord, 0) + } + return out, rows.Err() +} + +func (r *FaceGalleryRepo) AddPerson(name string) error { + db, err := r.open() + if err != nil { + return err + } + defer db.Close() + now := time.Now().Format(time.RFC3339) + _, err = db.Exec(`INSERT INTO person(name, created_at) VALUES(?, ?)`, name, now) + return err +} + +func (r *FaceGalleryRepo) DeletePerson(id int) error { + db, err := r.open() + if err != nil { + return err + } + defer db.Close() + _, err = db.Exec(`DELETE FROM embedding WHERE person_id = ?`, id) + if err != nil { + return err + } + _, err = db.Exec(`DELETE FROM person WHERE id = ?`, id) + return err +} + +func (r *FaceGalleryRepo) RenamePerson(id int, name string) error { + db, err := r.open() + if err != nil { + return err + } + defer db.Close() + _, err = db.Exec(`UPDATE person SET name = ? WHERE id = ?`, name, id) + return err +} diff --git a/internal/web/ui.go b/internal/web/ui.go index fd688fd..d7e31de 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -80,6 +80,7 @@ type PageData struct { StandardResources []storage.StandardResourceRecord ResourceStatusBoard *service.ResourceStatusBoard AlarmRecords []service.AlarmRecord + FaceGalleryPersons []storage.PersonRecord Templates []service.Template Template *service.Template AssetTab string @@ -661,6 +662,10 @@ func (u *UI) Routes() (chi.Router, error) { r.Post("/resources/sync", u.actionResourceSync) r.Get("/diagnostics", u.pageDiagnostics) r.Get("/alarms", u.pageAlarms) + r.Get("/face-gallery", u.pageFaceGallery) + r.Post("/face-gallery/add", u.actionFaceGalleryAdd) + r.Post("/face-gallery/delete", u.actionFaceGalleryDelete) + r.Post("/face-gallery/rename", u.actionFaceGalleryRename) r.Get("/monitor", u.pageMonitor) r.Get("/hls/*", u.proxyHLS) r.Get("/api/monitor/channels", u.apiMonitorChannels) @@ -3925,3 +3930,48 @@ func (u *UI) proxyHLS(w http.ResponseWriter, r *http.Request) { w.WriteHeader(code) w.Write(body) } + +func (u *UI) pageFaceGallery(w http.ResponseWriter, r *http.Request) { + data := PageData{Title: "人脸库管理"} + if repo := storage.NewFaceGalleryRepo(filepath.Join("resources", "standard_resources", "face_gallery", "face_gallery.db")); repo != nil { + if persons, err := repo.ListPersons(); err == nil { + data.FaceGalleryPersons = persons + } + } + u.render(w, r, "face_gallery", data) +} +func (u *UI) actionFaceGalleryAdd(w http.ResponseWriter, r *http.Request) { + name := strings.TrimSpace(r.FormValue("name")) + if name == "" { + http.Error(w, "name required", http.StatusBadRequest) + return + } + repo := storage.NewFaceGalleryRepo(filepath.Join("resources", "standard_resources", "face_gallery", "face_gallery.db")) + if err := repo.AddPerson(name); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/ui/face-gallery", http.StatusFound) +} +func (u *UI) actionFaceGalleryDelete(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(r.FormValue("id")) + if id <= 0 { + http.Error(w, "invalid id", http.StatusBadRequest) + return + } + repo := storage.NewFaceGalleryRepo(filepath.Join("resources", "standard_resources", "face_gallery", "face_gallery.db")) + repo.DeletePerson(id) + http.Redirect(w, r, "/ui/face-gallery", http.StatusFound) +} +func (u *UI) actionFaceGalleryRename(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(r.FormValue("id")) + name := strings.TrimSpace(r.FormValue("name")) + if id <= 0 || name == "" { + http.Error(w, "invalid", http.StatusBadRequest) + return + } + repo := storage.NewFaceGalleryRepo(filepath.Join("resources", "standard_resources", "face_gallery", "face_gallery.db")) + repo.RenamePerson(id, name) + http.Redirect(w, r, "/ui/face-gallery", http.StatusFound) +} + diff --git a/internal/web/ui/templates/face_gallery.html b/internal/web/ui/templates/face_gallery.html new file mode 100644 index 0000000..ef1a329 --- /dev/null +++ b/internal/web/ui/templates/face_gallery.html @@ -0,0 +1,57 @@ +{{define "face_gallery"}} +
| ID | 姓名 | 注册时间 | 操作 |
|---|---|---|---|
| {{.ID}} | ++ {{.Name}} + | +{{.CreatedAt}} | +
+
+
+
+
+ |
+