recordservice.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. CreatedAt string `json:"createdAt"` // 录入时间 YYYY-MM-DD HH:MM:SS(前端新增时生成;为空时后端用当前时间)
  25. }
  26. // openRecordDB 打开(或创建)记录数据库并确保表结构存在。
  27. // dbPath 为空时返回错误,提示用户到设置页配置。
  28. func openRecordDB(dbPath string) (*sql.DB, error) {
  29. dbPath = strings.TrimSpace(dbPath)
  30. if dbPath == "" {
  31. return nil, errors.New("未设置「记录数据库路径」,请到设置页配置后再使用")
  32. }
  33. if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
  34. return nil, err
  35. }
  36. // DSN 加 _pragma 参数:启用 WAL 提升并发读;foreign_keys 开启
  37. dsn := "file:" + filepath.ToSlash(dbPath) + "?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)"
  38. db, err := sql.Open("sqlite", dsn)
  39. if err != nil {
  40. return nil, err
  41. }
  42. if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS records (
  43. id INTEGER PRIMARY KEY AUTOINCREMENT,
  44. name TEXT NOT NULL DEFAULT '',
  45. item TEXT NOT NULL DEFAULT '',
  46. value REAL NOT NULL DEFAULT 0,
  47. created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
  48. )`); err != nil {
  49. db.Close()
  50. return nil, err
  51. }
  52. return db, nil
  53. }
  54. // ReadRecords 读取全部记录(按 id 升序,保持录入顺序)。
  55. // 前端 loadRecords 渲染列表;柱状图按返回顺序绘制。
  56. func (s *SettingsService) ReadRecords() ([]RecordItem, error) {
  57. db, err := openRecordDB(loadSettings().RecordDBPath)
  58. if err != nil {
  59. return nil, err
  60. }
  61. defer db.Close()
  62. rows, err := db.Query(`SELECT name, item, value, created_at FROM records ORDER BY id ASC`)
  63. if err != nil {
  64. return nil, err
  65. }
  66. defer rows.Close()
  67. items := make([]RecordItem, 0, 64)
  68. for rows.Next() {
  69. var it RecordItem
  70. if err := rows.Scan(&it.Name, &it.Item, &it.Value, &it.CreatedAt); err != nil {
  71. return nil, err
  72. }
  73. items = append(items, it)
  74. }
  75. return items, rows.Err()
  76. }
  77. // WriteRecords 全量替换记录:开启事务,先清空再批量插入。
  78. // 前端 saveRecords 提交当前列表全部行,保证与数据库完全一致。
  79. func (s *SettingsService) WriteRecords(items []RecordItem) error {
  80. db, err := openRecordDB(loadSettings().RecordDBPath)
  81. if err != nil {
  82. return err
  83. }
  84. defer db.Close()
  85. tx, err := db.Begin()
  86. if err != nil {
  87. return err
  88. }
  89. defer tx.Rollback()
  90. if _, err := tx.Exec(`DELETE FROM records`); err != nil {
  91. return err
  92. }
  93. stmt, err := tx.Prepare(`INSERT INTO records (name, item, value, created_at) VALUES (?, ?, ?, ?)`)
  94. if err != nil {
  95. return err
  96. }
  97. defer stmt.Close()
  98. now := time.Now().Format("2006-01-02 15:04:05")
  99. for _, it := range items {
  100. ts := strings.TrimSpace(it.CreatedAt)
  101. if ts == "" {
  102. ts = now // 前端未传时间时用当前时间(数据库默认值仅在列缺省时生效,显式插入需自行填充)
  103. }
  104. if _, err := stmt.Exec(strings.TrimSpace(it.Name), strings.TrimSpace(it.Item), it.Value, ts); err != nil {
  105. return err
  106. }
  107. }
  108. return tx.Commit()
  109. }
  110. // GetRecordNames 返回所有历史姓名(去重排序),供姓名输入框自动补全。
  111. func (s *SettingsService) GetRecordNames() ([]string, error) {
  112. return recordDistinctValues("name")
  113. }
  114. // GetRecordItems 返回所有历史名称(去重排序),供名称输入框自动补全。
  115. func (s *SettingsService) GetRecordItems() ([]string, error) {
  116. return recordDistinctValues("item")
  117. }
  118. // recordDistinctValues 查询某列去重后的非空值(排序),供自动补全候选。
  119. func recordDistinctValues(col string) ([]string, error) {
  120. // col 仅允许 name/item 白名单,避免 SQL 注入
  121. if col != "name" && col != "item" {
  122. return nil, fmt.Errorf("非法列名: %s", col)
  123. }
  124. db, err := openRecordDB(loadSettings().RecordDBPath)
  125. if err != nil {
  126. return nil, err
  127. }
  128. defer db.Close()
  129. rows, err := db.Query(`SELECT DISTINCT ` + col + ` FROM records WHERE TRIM(` + col + `) <> '' ORDER BY ` + col + ` COLLATE NOCASE ASC`)
  130. if err != nil {
  131. return nil, err
  132. }
  133. defer rows.Close()
  134. vals := make([]string, 0, 32)
  135. for rows.Next() {
  136. var v string
  137. if err := rows.Scan(&v); err != nil {
  138. return nil, err
  139. }
  140. vals = append(vals, v)
  141. }
  142. return vals, rows.Err()
  143. }