Files
kevinandClaude eff4972668 重构:拆出 internal/config 与 internal/store 子包
把工程根目录中和"配置加载"、"数据存储"两个领域相关的全部 Go 文件迁到
internal/ 下的子包,按功能分组的第一阶段成果。

internal/config/
- 从 config.go 抽出 Config / MQTTConfig / WebConfig / DatabaseConfig 等
  类型并大写导出,函数改名 Default/Load/Write/Validate/BuildTLS 等。
- 测试随被测代码迁移成 package config 的内部测试。
- 根目录留 config.go 作桥接:用 type alias 让旧的小写名(config /
  mqttConfig / databaseConfig 等)继续可用,避免修改 30+ 处调用方。

internal/store/
- 把 db.go、store_query.go、db_write_queue.go 与 13 个 *_store.go 一并
  迁入;26 个 *Record 类型与 store 同包以避免循环依赖。
- store -> Store;50+ 标识符从小写未导出改为大写导出(包括 record、
  ListOptions、错误变量、bot/llm/runtime 常量、helpers 等)。
- 新增 DB() / Driver() 访问器供 ai 子系统使用,避免直接访问私有字段。
- bot_pki_store.go 独立出来,把 PKI 解密所需的 store 方法集中归类。
- helpers.go 提供 hashPassword / uint32FromRecord / printJSON 等以前在
  其他根目录文件中的辅助;test_helpers_test.go 提供 verifyPassword
  与 publicMapTileSourceDTO 让测试可以本地运行而不依赖 main 包。

根目录新增:
- store_bridge.go:完整 type-alias / 函数包装层,把 internal/store 的
  导出名映射回旧的小写名,让 admin_*_routes.go、web.go、bot_service.go
  等仍未迁出的文件继续编译。后续步骤把它们迁到各自领域包后可逐步删除。
- test_helpers_test.go:根目录测试沿用 openTestStore 的入口。

go build ./... 与 go test ./... 全部通过;测试数量与重构前一致。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-18 13:56:36 +08:00

326 lines
9.4 KiB
Go

package store
import (
"errors"
"fmt"
"net"
"strings"
"time"
"gorm.io/gorm"
)
const ForbiddenWordMatchContains = "contains"
var ErrBlockingAlreadyExists = errors.New("blocking rule already exists")
func (s *Store) ListNodeBlocking(opts ListOptions) ([]NodeBlockingRecord, error) {
opts = NormalizeListOptions(opts)
var rows []NodeBlockingRecord
q := s.db.Model(&NodeBlockingRecord{}).
Order("updated_at DESC").
Order("id DESC").
Limit(opts.Limit).
Offset(opts.Offset)
return rows, q.Find(&rows).Error
}
func (s *Store) CountNodeBlocking(opts ListOptions) (int64, error) {
var total int64
return total, s.db.Model(&NodeBlockingRecord{}).Count(&total).Error
}
func (s *Store) ListEnabledNodeBlocking() ([]NodeBlockingRecord, error) {
var rows []NodeBlockingRecord
return rows, s.db.Where("enabled = ?", true).Find(&rows).Error
}
func (s *Store) CreateNodeBlocking(nodeID string, nodeNum *int64, reason string, enabled bool) (*NodeBlockingRecord, error) {
nodeID = strings.TrimSpace(nodeID)
if nodeID == "" {
return nil, fmt.Errorf("node id is required")
}
if err := s.ensureNodeBlockingUnique(0, nodeID); err != nil {
return nil, err
}
row := NodeBlockingRecord{NodeID: nodeID, NodeNum: nodeNum, Reason: strings.TrimSpace(reason), Enabled: enabled}
if err := s.db.Create(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func (s *Store) UpdateNodeBlocking(id uint64, nodeID string, nodeNum *int64, reason string, enabled bool) (*NodeBlockingRecord, error) {
if id == 0 {
return nil, fmt.Errorf("blocking rule id is required")
}
nodeID = strings.TrimSpace(nodeID)
if nodeID == "" {
return nil, fmt.Errorf("node id is required")
}
if _, err := s.getNodeBlockingByID(id); err != nil {
return nil, err
}
if err := s.ensureNodeBlockingUnique(id, nodeID); err != nil {
return nil, err
}
updates := map[string]any{"node_id": nodeID, "node_num": nodeNum, "reason": strings.TrimSpace(reason), "enabled": enabled, "updated_at": time.Now()}
if err := s.db.Model(&NodeBlockingRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return nil, err
}
return s.getNodeBlockingByID(id)
}
func (s *Store) DeleteNodeBlocking(id uint64) error {
result := s.db.Where("id = ?", id).Delete(&NodeBlockingRecord{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (s *Store) ListIPBlocking(opts ListOptions) ([]IPBlockingRecord, error) {
opts = NormalizeListOptions(opts)
var rows []IPBlockingRecord
q := s.db.Model(&IPBlockingRecord{}).
Order("updated_at DESC").
Order("id DESC").
Limit(opts.Limit).
Offset(opts.Offset)
return rows, q.Find(&rows).Error
}
func (s *Store) CountIPBlocking(opts ListOptions) (int64, error) {
var total int64
return total, s.db.Model(&IPBlockingRecord{}).Count(&total).Error
}
func (s *Store) ListEnabledIPBlocking() ([]IPBlockingRecord, error) {
var rows []IPBlockingRecord
return rows, s.db.Where("enabled = ?", true).Find(&rows).Error
}
func (s *Store) CreateIPBlocking(ipValue string, reason string, enabled bool) (*IPBlockingRecord, error) {
value, err := normalizeIPBlockingValue(ipValue)
if err != nil {
return nil, err
}
if err := s.ensureIPBlockingUnique(0, value); err != nil {
return nil, err
}
row := IPBlockingRecord{IPValue: value, Reason: strings.TrimSpace(reason), Enabled: enabled}
if err := s.db.Create(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func (s *Store) UpdateIPBlocking(id uint64, ipValue string, reason string, enabled bool) (*IPBlockingRecord, error) {
if id == 0 {
return nil, fmt.Errorf("blocking rule id is required")
}
value, err := normalizeIPBlockingValue(ipValue)
if err != nil {
return nil, err
}
if _, err := s.getIPBlockingByID(id); err != nil {
return nil, err
}
if err := s.ensureIPBlockingUnique(id, value); err != nil {
return nil, err
}
updates := map[string]any{"ip_value": value, "reason": strings.TrimSpace(reason), "enabled": enabled, "updated_at": time.Now()}
if err := s.db.Model(&IPBlockingRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return nil, err
}
return s.getIPBlockingByID(id)
}
func (s *Store) DeleteIPBlocking(id uint64) error {
result := s.db.Where("id = ?", id).Delete(&IPBlockingRecord{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (s *Store) ListForbiddenWordBlocking(opts ListOptions) ([]ForbiddenWordBlockingRecord, error) {
opts = NormalizeListOptions(opts)
var rows []ForbiddenWordBlockingRecord
q := s.db.Model(&ForbiddenWordBlockingRecord{}).
Order("updated_at DESC").
Order("id DESC").
Limit(opts.Limit).
Offset(opts.Offset)
return rows, q.Find(&rows).Error
}
func (s *Store) CountForbiddenWordBlocking(opts ListOptions) (int64, error) {
var total int64
return total, s.db.Model(&ForbiddenWordBlockingRecord{}).Count(&total).Error
}
func (s *Store) ListEnabledForbiddenWordBlocking() ([]ForbiddenWordBlockingRecord, error) {
var rows []ForbiddenWordBlockingRecord
return rows, s.db.Where("enabled = ?", true).Find(&rows).Error
}
func (s *Store) CreateForbiddenWordBlocking(word, matchType string, caseSensitive bool, reason string, enabled bool) (*ForbiddenWordBlockingRecord, error) {
word = strings.TrimSpace(word)
if word == "" {
return nil, fmt.Errorf("forbidden word is required")
}
matchType, err := normalizeForbiddenWordMatchType(matchType)
if err != nil {
return nil, err
}
if err := s.ensureForbiddenWordBlockingUnique(0, word); err != nil {
return nil, err
}
row := ForbiddenWordBlockingRecord{Word: word, MatchType: matchType, CaseSensitive: caseSensitive, Reason: strings.TrimSpace(reason), Enabled: enabled}
if err := s.db.Create(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func (s *Store) UpdateForbiddenWordBlocking(id uint64, word, matchType string, caseSensitive bool, reason string, enabled bool) (*ForbiddenWordBlockingRecord, error) {
if id == 0 {
return nil, fmt.Errorf("blocking rule id is required")
}
word = strings.TrimSpace(word)
if word == "" {
return nil, fmt.Errorf("forbidden word is required")
}
matchType, err := normalizeForbiddenWordMatchType(matchType)
if err != nil {
return nil, err
}
if _, err := s.getForbiddenWordBlockingByID(id); err != nil {
return nil, err
}
if err := s.ensureForbiddenWordBlockingUnique(id, word); err != nil {
return nil, err
}
updates := map[string]any{"word": word, "match_type": matchType, "case_sensitive": caseSensitive, "reason": strings.TrimSpace(reason), "enabled": enabled, "updated_at": time.Now()}
if err := s.db.Model(&ForbiddenWordBlockingRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return nil, err
}
return s.getForbiddenWordBlockingByID(id)
}
func (s *Store) DeleteForbiddenWordBlocking(id uint64) error {
result := s.db.Where("id = ?", id).Delete(&ForbiddenWordBlockingRecord{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (s *Store) getNodeBlockingByID(id uint64) (*NodeBlockingRecord, error) {
var row NodeBlockingRecord
if err := s.db.Where("id = ?", id).Take(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func (s *Store) getIPBlockingByID(id uint64) (*IPBlockingRecord, error) {
var row IPBlockingRecord
if err := s.db.Where("id = ?", id).Take(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func (s *Store) getForbiddenWordBlockingByID(id uint64) (*ForbiddenWordBlockingRecord, error) {
var row ForbiddenWordBlockingRecord
if err := s.db.Where("id = ?", id).Take(&row).Error; err != nil {
return nil, err
}
return &row, nil
}
func (s *Store) ensureNodeBlockingUnique(id uint64, nodeID string) error {
var existing NodeBlockingRecord
q := s.db.Where("node_id = ?", nodeID)
if id != 0 {
q = q.Where("id <> ?", id)
}
err := q.Take(&existing).Error
if err == nil {
return ErrBlockingAlreadyExists
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
func (s *Store) ensureIPBlockingUnique(id uint64, ipValue string) error {
var existing IPBlockingRecord
q := s.db.Where("ip_value = ?", ipValue)
if id != 0 {
q = q.Where("id <> ?", id)
}
err := q.Take(&existing).Error
if err == nil {
return ErrBlockingAlreadyExists
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
func (s *Store) ensureForbiddenWordBlockingUnique(id uint64, word string) error {
var existing ForbiddenWordBlockingRecord
q := s.db.Where("word = ?", word)
if id != 0 {
q = q.Where("id <> ?", id)
}
err := q.Take(&existing).Error
if err == nil {
return ErrBlockingAlreadyExists
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
func normalizeIPBlockingValue(value string) (string, error) {
value = strings.TrimSpace(value)
if value == "" {
return "", fmt.Errorf("ip value is required")
}
if ip := net.ParseIP(value); ip != nil {
return ip.String(), nil
}
_, ipNet, err := net.ParseCIDR(value)
if err == nil {
return ipNet.String(), nil
}
return "", fmt.Errorf("ip value must be a valid IP or CIDR")
}
func normalizeForbiddenWordMatchType(matchType string) (string, error) {
matchType = strings.TrimSpace(matchType)
if matchType == "" {
return ForbiddenWordMatchContains, nil
}
if matchType != ForbiddenWordMatchContains {
return "", fmt.Errorf("unsupported forbidden word match type")
}
return matchType, nil
}