// ============================================================================ // recordservice.go — 记录(Record)SQLite 存储 // // 记录 Tab 的数据持久化:每行一条「姓名|名称|值」,全部保存到 SQLite3 数据库 // (modernc.org/sqlite,纯 Go 驱动,无 CGO)。姓名/名称输入框自动补全的候选 // 来自数据库中已存在的历史值(去重)。 // ============================================================================ package main import ( "database/sql" "errors" "fmt" "os" "path/filepath" "strings" "time" _ "modernc.org/sqlite" // 纯 Go SQLite 驱动(无需 CGO) ) // RecordItem 一条记录:姓名 + 名称 + 值 + 录入时间,供记录 Tab 列表与柱状图使用 type RecordItem struct { Name string `json:"name"` // 姓名 Item string `json:"item"` // 名称 Value float64 `json:"value"` // 值 CreatedAt string `json:"createdAt"` // 录入时间 YYYY-MM-DD HH:MM:SS(前端新增时生成;为空时后端用当前时间) } // openRecordDB 打开(或创建)记录数据库并确保表结构存在。 // dbPath 为空时返回错误,提示用户到设置页配置。 func openRecordDB(dbPath string) (*sql.DB, error) { dbPath = strings.TrimSpace(dbPath) if dbPath == "" { return nil, errors.New("未设置「记录数据库路径」,请到设置页配置后再使用") } if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil { return nil, err } // DSN 加 _pragma 参数:启用 WAL 提升并发读;foreign_keys 开启 dsn := "file:" + filepath.ToSlash(dbPath) + "?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)" db, err := sql.Open("sqlite", dsn) if err != nil { return nil, err } if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL DEFAULT '', item TEXT NOT NULL DEFAULT '', value REAL NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')) )`); err != nil { db.Close() return nil, err } return db, nil } // ReadRecords 读取全部记录(按 id 升序,保持录入顺序)。 // 前端 loadRecords 渲染列表;柱状图按返回顺序绘制。 func (s *SettingsService) ReadRecords() ([]RecordItem, error) { db, err := openRecordDB(loadSettings().RecordDBPath) if err != nil { return nil, err } defer db.Close() rows, err := db.Query(`SELECT name, item, value, created_at FROM records ORDER BY id ASC`) if err != nil { return nil, err } defer rows.Close() items := make([]RecordItem, 0, 64) for rows.Next() { var it RecordItem if err := rows.Scan(&it.Name, &it.Item, &it.Value, &it.CreatedAt); err != nil { return nil, err } items = append(items, it) } return items, rows.Err() } // WriteRecords 全量替换记录:开启事务,先清空再批量插入。 // 前端 saveRecords 提交当前列表全部行,保证与数据库完全一致。 func (s *SettingsService) WriteRecords(items []RecordItem) error { db, err := openRecordDB(loadSettings().RecordDBPath) if err != nil { return err } defer db.Close() tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec(`DELETE FROM records`); err != nil { return err } stmt, err := tx.Prepare(`INSERT INTO records (name, item, value, created_at) VALUES (?, ?, ?, ?)`) if err != nil { return err } defer stmt.Close() now := time.Now().Format("2006-01-02 15:04:05") for _, it := range items { ts := strings.TrimSpace(it.CreatedAt) if ts == "" { ts = now // 前端未传时间时用当前时间(数据库默认值仅在列缺省时生效,显式插入需自行填充) } if _, err := stmt.Exec(strings.TrimSpace(it.Name), strings.TrimSpace(it.Item), it.Value, ts); err != nil { return err } } return tx.Commit() } // GetRecordNames 返回所有历史姓名(去重排序),供姓名输入框自动补全。 func (s *SettingsService) GetRecordNames() ([]string, error) { return recordDistinctValues("name") } // GetRecordItems 返回所有历史名称(去重排序),供名称输入框自动补全。 func (s *SettingsService) GetRecordItems() ([]string, error) { return recordDistinctValues("item") } // recordDistinctValues 查询某列去重后的非空值(排序),供自动补全候选。 func recordDistinctValues(col string) ([]string, error) { // col 仅允许 name/item 白名单,避免 SQL 注入 if col != "name" && col != "item" { return nil, fmt.Errorf("非法列名: %s", col) } db, err := openRecordDB(loadSettings().RecordDBPath) if err != nil { return nil, err } defer db.Close() rows, err := db.Query(`SELECT DISTINCT ` + col + ` FROM records WHERE TRIM(` + col + `) <> '' ORDER BY ` + col + ` COLLATE NOCASE ASC`) if err != nil { return nil, err } defer rows.Close() vals := make([]string, 0, 32) for rows.Next() { var v string if err := rows.Scan(&v); err != nil { return nil, err } vals = append(vals, v) } return vals, rows.Err() }