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:
w1ndyb0y
2026-07-10 15:32:34 +08:00
parent 5f3334ec87
commit 419f25f0dd
20 changed files with 1519 additions and 206 deletions
+35 -59
View File
@@ -25,43 +25,39 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/spf13/viper"
"it2000.com.cn/tele-recv/config"
"it2000.com.cn/tele-recv/utils"
)
const ModeName = "TELEGRAM_MODE"
var (
cfgFile string
device string
baudrate int
lograw bool
dbFile string
dbInit bool
cfgFile string
// Legacy global vars for backward compatibility with test command
device string
baudrate int
dbFile string
dbInit bool
socketAddress string
pulsarUrl string
topic string
name string
tcp bool
pulsar bool
lograw bool
pulsarUrl string
topic string
name string
tcp bool
pulsar bool
Mode string
// rootCmd represents the base command when called without any subcommands
rootCmd = &cobra.Command{
Use: "tele-recv",
Short: "telegram receiver",
Long: `A serial port telegram receiver.
Read the telegram from `,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
Read the telegram from serial port`,
}
Mode = os.Getenv(ModeName)
appCfg *config.Config
logger *zap.Logger
)
@@ -77,55 +73,36 @@ func Execute() {
func init() {
cobra.OnInitialize(initConfig)
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (telegram.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
initLog()
defer logger.Sync()
// l = logger.Sugar()
utils.Log = logger
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Search config in home directory with name ".tele-recv" (without extension).
viper.AddConfigPath(".")
viper.SetConfigName("telegram")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
// fmt.Println("Using config file:", viper.ConfigFileUsed())
logger.Info("Using config file ", zap.String("path", viper.ConfigFileUsed()))
device = viper.GetString("serial.device")
baudrate = viper.GetInt("serial.baudrate")
lograw = viper.GetBool("serial.lograw")
dbFile = viper.GetString("sqlite.file")
dbInit = viper.GetBool("sqlite.init")
socketAddress = viper.GetString("socket.address")
pulsarUrl = viper.GetString("pulsar.url")
topic = viper.GetString("pulsar.topic")
name = viper.GetString("pulsar.name")
tcp = viper.GetBool("telegram.tcp")
utils.Tcp = tcp
pulsar = viper.GetBool("telegram.pulsar")
utils.Pulsar = pulsar
cfg, err := config.Load(cfgFile)
if err != nil {
logger.Warn("config load error, using defaults", zap.Error(err))
return
}
appCfg = cfg
// Populate legacy globals for backward compatibility
device = cfg.Serial.Device
baudrate = cfg.Serial.Baudrate
lograw = cfg.Serial.LogRaw
dbFile = cfg.SQLite.File
dbInit = cfg.SQLite.Init
socketAddress = cfg.Socket.Address
pulsarUrl = cfg.Pulsar.URL
topic = cfg.Pulsar.Topic
name = cfg.Pulsar.Name
tcp = cfg.Telegram.TCP
pulsar = cfg.Telegram.Pulsar
utils.Tcp = tcp
utils.Pulsar = pulsar
}
func initLog() {
@@ -163,5 +140,4 @@ func initLog() {
)
logger = zap.New(core)
}
+14 -80
View File
@@ -16,15 +16,10 @@ limitations under the License.
package cmd
import (
"fmt"
"os"
"os/signal"
"syscall"
"go.uber.org/zap"
"it2000.com.cn/tele-recv/utils"
"github.com/spf13/cobra"
"go.uber.org/zap"
"it2000.com.cn/tele-recv/app"
)
// startCmd represents the start command
@@ -33,85 +28,24 @@ var startCmd = &cobra.Command{
Short: "start telegram receive service",
Long: `open serial port
start a tcp server for processing`,
Run: func(cmd *cobra.Command, args []string) {
start()
RunE: func(cmd *cobra.Command, args []string) error {
return start()
},
}
func init() {
rootCmd.AddCommand(startCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// startCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
func start() {
// Go signal notification works by sending `os.Signal`
// values on a channel. We'll create a channel to
// receive these notifications (we'll also make one to
// notify us when the program can exit).
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)
// `signal.Notify` registers the given channel to
// receive notifications of the specified signals.
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
utils.ServerRunning = true
// This goroutine executes a blocking receive for
// signals. When it gets one it'll print it out
// and then notify the program that it can finish.
go func() {
sig := <-sigs
logger.Info("got ", zap.Any("signal", sig))
utils.ServerRunning = false
if tcp {
utils.StopSocketServer()
}
if pulsar {
utils.ClosePulsar()
}
done <- true
}()
// The program will wait here until it gets the
// expected signal (as indicated by the goroutine
// above sending a value on `done`) and then exit.
logger.Info("awaiting signal")
_ = utils.InitDb(dbFile, dbInit)
if tcp {
go utils.Listen(socketAddress)
func start() error {
if appCfg == nil {
logger.Fatal("configuration not loaded")
}
if pulsar {
utils.CreateProducer(pulsarUrl, topic, name)
}
for utils.ServerRunning {
if !utils.IsPortOpen() {
logger.Info("try to open port")
err := utils.OpenPort(device, baudrate)
if err != nil {
logger.Fatal("error in open serial port ", zap.Error(err))
}
logger.Info("starting read")
}
buffer, err := utils.ReadPort()
if err == nil {
if lograw && len(buffer) > 0 {
fmt.Println(buffer)
}
if utils.Append(buffer) {
fmt.Println()
}
}
}
<-done
logger.Info("exiting")
a, err := app.New(appCfg)
if err != nil {
logger.Fatal("failed to create app", zap.Error(err))
}
return a.Run()
}
-10
View File
@@ -47,14 +47,4 @@ func test() {
func init() {
rootCmd.AddCommand(testCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// testCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// testCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}