| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- // ============================================================================
- // 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"` // 值
- Remark string `json:"remark"` // 备注(可选)
- 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 开启;busy_timeout 让写锁等待而非立即报「database is locked」
- dsn := "file:" + filepath.ToSlash(dbPath) + "?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(5000)"
- 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,
- remark TEXT NOT NULL DEFAULT '',
- created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
- )`); err != nil {
- db.Close()
- return nil, err
- }
- // 兼容旧库:若 records 表已存在但缺 remark 列(早期版本无备注),执行迁移补列
- if _, err := db.Exec(`ALTER TABLE records ADD COLUMN remark TEXT NOT NULL DEFAULT ''`); err != 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, remark, 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.Remark, &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, remark, 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, strings.TrimSpace(it.Remark), 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()
- }
|