recordservice.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. // ============================================================================
  2. // recordservice.go — 记录(Record)SQLite 存储
  3. //
  4. // 记录 Tab 的数据持久化:每行一条「姓名|名称|值」,全部保存到 SQLite3 数据库
  5. // (modernc.org/sqlite,纯 Go 驱动,无 CGO)。姓名/名称输入框自动补全的候选
  6. // 来自数据库中已存在的历史值(去重)。
  7. // ============================================================================
  8. package main
  9. import (
  10. "database/sql"
  11. "errors"
  12. "fmt"
  13. "os"
  14. "path/filepath"
  15. "strings"
  16. "time"
  17. _ "modernc.org/sqlite" // 纯 Go SQLite 驱动(无需 CGO)
  18. )
  19. // RecordItem 一条记录:姓名 + 名称 + 值 + 备注 + 录入时间,供记录 Tab 列表与柱状图使用
  20. type RecordItem struct {
  21. Name string `json:"name"` // 姓名
  22. Item string `json:"item"` // 名称
  23. Value float64 `json:"value"` // 值
  24. Remark string `json:"remark"` // 备注(可选)
  25. CreatedAt string `json:"createdAt"` // 录入时间 YYYY-MM-DD HH:MM:SS(前端新增时生成;为空时后端用当前时间)
  26. }
  27. // openRecordDB 打开(或创建)记录数据库并确保表结构存在。
  28. // dbPath 为空时返回错误,提示用户到设置页配置。
  29. func openRecordDB(dbPath string) (*sql.DB, error) {
  30. dbPath = strings.TrimSpace(dbPath)
  31. if dbPath == "" {
  32. return nil, errors.New("未设置「记录数据库路径」,请到设置页配置后再使用")
  33. }
  34. if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
  35. return nil, err
  36. }
  37. // DSN 加 _pragma 参数:启用 WAL 提升并发读;foreign_keys 开启;busy_timeout 让写锁等待而非立即报「database is locked」
  38. dsn := "file:" + filepath.ToSlash(dbPath) + "?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(5000)"
  39. db, err := sql.Open("sqlite", dsn)
  40. if err != nil {
  41. return nil, err
  42. }
  43. if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS records (
  44. id INTEGER PRIMARY KEY AUTOINCREMENT,
  45. name TEXT NOT NULL DEFAULT '',
  46. item TEXT NOT NULL DEFAULT '',
  47. value REAL NOT NULL DEFAULT 0,
  48. remark TEXT NOT NULL DEFAULT '',
  49. created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
  50. )`); err != nil {
  51. db.Close()
  52. return nil, err
  53. }
  54. // 兼容旧库:若 records 表已存在但缺 remark 列(早期版本无备注),执行迁移补列
  55. if _, err := db.Exec(`ALTER TABLE records ADD COLUMN remark TEXT NOT NULL DEFAULT ''`); err != nil {
  56. // 列已存在(重复添加)会报错,忽略;其他错误也仅忽略——迁移为尽力而为
  57. _ = err
  58. }
  59. return db, nil
  60. }
  61. // ReadRecords 读取全部记录(按 id 升序,保持录入顺序)。
  62. // 前端 loadRecords 渲染列表;柱状图按返回顺序绘制。
  63. func (s *SettingsService) ReadRecords() ([]RecordItem, error) {
  64. db, err := openRecordDB(loadSettings().RecordDBPath)
  65. if err != nil {
  66. return nil, err
  67. }
  68. defer db.Close()
  69. rows, err := db.Query(`SELECT name, item, value, remark, created_at FROM records ORDER BY id ASC`)
  70. if err != nil {
  71. return nil, err
  72. }
  73. defer rows.Close()
  74. items := make([]RecordItem, 0, 64)
  75. for rows.Next() {
  76. var it RecordItem
  77. if err := rows.Scan(&it.Name, &it.Item, &it.Value, &it.Remark, &it.CreatedAt); err != nil {
  78. return nil, err
  79. }
  80. items = append(items, it)
  81. }
  82. return items, rows.Err()
  83. }
  84. // WriteRecords 全量替换记录:开启事务,先清空再批量插入。
  85. // 前端 saveRecords 提交当前列表全部行,保证与数据库完全一致。
  86. func (s *SettingsService) WriteRecords(items []RecordItem) error {
  87. db, err := openRecordDB(loadSettings().RecordDBPath)
  88. if err != nil {
  89. return err
  90. }
  91. defer db.Close()
  92. tx, err := db.Begin()
  93. if err != nil {
  94. return err
  95. }
  96. defer tx.Rollback()
  97. if _, err := tx.Exec(`DELETE FROM records`); err != nil {
  98. return err
  99. }
  100. stmt, err := tx.Prepare(`INSERT INTO records (name, item, value, remark, created_at) VALUES (?, ?, ?, ?, ?)`)
  101. if err != nil {
  102. return err
  103. }
  104. defer stmt.Close()
  105. now := time.Now().Format("2006-01-02 15:04:05")
  106. for _, it := range items {
  107. ts := strings.TrimSpace(it.CreatedAt)
  108. if ts == "" {
  109. ts = now // 前端未传时间时用当前时间(数据库默认值仅在列缺省时生效,显式插入需自行填充)
  110. }
  111. if _, err := stmt.Exec(strings.TrimSpace(it.Name), strings.TrimSpace(it.Item), it.Value, strings.TrimSpace(it.Remark), ts); err != nil {
  112. return err
  113. }
  114. }
  115. return tx.Commit()
  116. }
  117. // GetRecordNames 返回所有历史姓名(去重排序),供姓名输入框自动补全。
  118. func (s *SettingsService) GetRecordNames() ([]string, error) {
  119. return recordDistinctValues("name")
  120. }
  121. // GetRecordItems 返回所有历史名称(去重排序),供名称输入框自动补全。
  122. func (s *SettingsService) GetRecordItems() ([]string, error) {
  123. return recordDistinctValues("item")
  124. }
  125. // recordDistinctValues 查询某列去重后的非空值(排序),供自动补全候选。
  126. func recordDistinctValues(col string) ([]string, error) {
  127. // col 仅允许 name/item 白名单,避免 SQL 注入
  128. if col != "name" && col != "item" {
  129. return nil, fmt.Errorf("非法列名: %s", col)
  130. }
  131. db, err := openRecordDB(loadSettings().RecordDBPath)
  132. if err != nil {
  133. return nil, err
  134. }
  135. defer db.Close()
  136. rows, err := db.Query(`SELECT DISTINCT ` + col + ` FROM records WHERE TRIM(` + col + `) <> '' ORDER BY ` + col + ` COLLATE NOCASE ASC`)
  137. if err != nil {
  138. return nil, err
  139. }
  140. defer rows.Close()
  141. vals := make([]string, 0, 32)
  142. for rows.Next() {
  143. var v string
  144. if err := rows.Scan(&v); err != nil {
  145. return nil, err
  146. }
  147. vals = append(vals, v)
  148. }
  149. return vals, rows.Err()
  150. }