fix: comprehensive bug fixes and architecture restructuring
Phase 1 — Bug fixes (7 bugs): - Bug 1: Pulsar mode data loss — always persist to SQLite regardless of mode - Bug 2: PulsarSend closing TCP client — use independent resetPulsarProducer() - Bug 3: Greedy regex — use non-greedy (?s)ZCZC.*?NNNN - Bug 4: Data after NNNN discarded — keep remaining buffer data - Bug 5: strings.Index > 0 boundary — use strings.Contains - Bug 6: Variable shadowing in PulsarSend — use = not := - Bug 7: Accept failure nil panic — add continue + retry logic Phase 2 — Architecture restructuring: - Split utils/ into config/, serial/, telegram/, storage/, transport/ - Define Sender, Repository, Reader interfaces - Introduce app/ layer with context.Context lifecycle - Replace spinlock with sync.Mutex - Unified Config struct replaces 15+ global vars Phase 3 — Testing & tooling: - telegram/parser_test.go (7 test cases) - storage/store_test.go (5 test cases) - transport/transport_test.go (4 test cases) - Taskfile: add test, test-race, test-cover tasks - Go version: 1.15 -> 1.21
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Repository defines the interface for telegram persistence.
|
||||
type Repository interface {
|
||||
Insert(telegram string) error
|
||||
LoadUnprocessed() ([]Telegram, error)
|
||||
MarkProcessed(id int64) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Telegram represents a stored telegram record.
|
||||
type Telegram struct {
|
||||
ID int64
|
||||
Text string
|
||||
}
|
||||
|
||||
// Store implements Repository using SQLite.
|
||||
type Store struct {
|
||||
mu sync.Mutex
|
||||
dbFile string
|
||||
db *sql.DB
|
||||
initTable bool
|
||||
}
|
||||
|
||||
const (
|
||||
driverName = "sqlite3"
|
||||
tableDDL = `
|
||||
create table IF NOT EXISTS telegram (
|
||||
[tele_id] INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
[tele_recv_time] TIMESTAMP NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
[tele_processed] int(1) NOT NULL DEFAULT 0,
|
||||
[tele_text] TEXT NOT NULL
|
||||
)
|
||||
`
|
||||
insertSQL = "insert into telegram (tele_text) values (?)"
|
||||
countSQL = "select count(*) from telegram where tele_processed=0"
|
||||
loadSQL = "select tele_id, tele_text from telegram where tele_processed=0 Limit 100"
|
||||
updateSQL = "update telegram set tele_processed = 1 where tele_id=?"
|
||||
)
|
||||
|
||||
// New opens or creates a SQLite store.
|
||||
func New(dbFile string, init bool) (*Store, error) {
|
||||
if init {
|
||||
os.Remove(dbFile)
|
||||
}
|
||||
|
||||
db, err := sql.Open(driverName, dbFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := db.Exec(tableDDL); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Store{
|
||||
dbFile: dbFile,
|
||||
db: db,
|
||||
initTable: init,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Insert saves a telegram to the database.
|
||||
func (s *Store) Insert(telegram string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
stmt, err := s.db.Prepare(insertSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(telegram)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadUnprocessed returns up to 100 unprocessed telegrams.
|
||||
func (s *Store) LoadUnprocessed() ([]Telegram, error) {
|
||||
rows, err := s.db.Query(loadSQL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var telegrams []Telegram
|
||||
for rows.Next() {
|
||||
var t Telegram
|
||||
if err := rows.Scan(&t.ID, &t.Text); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
telegrams = append(telegrams, t)
|
||||
}
|
||||
return telegrams, nil
|
||||
}
|
||||
|
||||
// MarkProcessed marks a telegram as processed.
|
||||
func (s *Store) MarkProcessed(id int64) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
stmt, err := s.db.Prepare(updateSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(id)
|
||||
return err
|
||||
}
|
||||
|
||||
// CountUnprocessed returns the number of unprocessed telegrams.
|
||||
func (s *Store) CountUnprocessed() (int64, error) {
|
||||
var count int64
|
||||
err := s.db.QueryRow(countSQL).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStore_InsertAndCount(t *testing.T) {
|
||||
dbFile := "test_telegram.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
s, err := New(dbFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
if err := s.Insert("ZCZC TEST NNNN"); err != nil {
|
||||
t.Fatalf("Insert() failed: %v", err)
|
||||
}
|
||||
|
||||
count, err := s.CountUnprocessed()
|
||||
if err != nil {
|
||||
t.Fatalf("CountUnprocessed() failed: %v", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("expected 1 unprocessed, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_LoadUnprocessed(t *testing.T) {
|
||||
dbFile := "test_load.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
s, err := New(dbFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
s.Insert("ZCZC MSG1 NNNN")
|
||||
s.Insert("ZCZC MSG2 NNNN")
|
||||
|
||||
telegrams, err := s.LoadUnprocessed()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadUnprocessed() failed: %v", err)
|
||||
}
|
||||
if len(telegrams) != 2 {
|
||||
t.Errorf("expected 2 telegrams, got %d", len(telegrams))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_MarkProcessed(t *testing.T) {
|
||||
dbFile := "test_mark.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
s, err := New(dbFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
s.Insert("ZCZC TEST NNNN")
|
||||
|
||||
telegrams, _ := s.LoadUnprocessed()
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram, got %d", len(telegrams))
|
||||
}
|
||||
|
||||
if err := s.MarkProcessed(telegrams[0].ID); err != nil {
|
||||
t.Fatalf("MarkProcessed() failed: %v", err)
|
||||
}
|
||||
|
||||
count, _ := s.CountUnprocessed()
|
||||
if count != 0 {
|
||||
t.Errorf("expected 0 unprocessed after marking, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_Empty(t *testing.T) {
|
||||
dbFile := "test_empty.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
s, err := New(dbFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
telegrams, err := s.LoadUnprocessed()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadUnprocessed() failed: %v", err)
|
||||
}
|
||||
if len(telegrams) != 0 {
|
||||
t.Errorf("expected 0 telegrams, got %d", len(telegrams))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user