Pārlūkot izejas kodu

更新配置文件读取并支持自动重载

重构了配置文件的读取逻辑,引入了viper库,并实现了配置文件变更时的自动重载功能。此外,新增了控制台模式选择和程序重启机制以支持调试。
SongZihuan 1 nedēļu atpakaļ
vecāks
revīzija
14e3e73cf1

+ 7 - 0
CHANGELOG.md

@@ -7,6 +7,13 @@
 
 **注意:本文档内容若与[GitHub Wiki](https://github.com/SongZihuan/BackendServerTemplate/wiki/%E5%8F%98%E6%9B%B4%E6%97%A5%E5%BF%97)冲突,则以后者为准**
 
+## [未发布]
+
+### 重构
+
+- 使用`viper`重构配置文件读取。
+- 支持文件重载后,完整重启系统(用于调试功能)。
+
 ## [0.7.0] - 2025-04-21
 
 ### 修复

+ 13 - 1
go.mod

@@ -11,8 +11,20 @@ require (
 )
 
 require (
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+	github.com/sagikazarmark/locafero v0.9.0 // indirect
+	github.com/sourcegraph/conc v0.3.0 // indirect
+	github.com/spf13/afero v1.14.0 // indirect
+	github.com/spf13/cast v1.7.1 // indirect
 	github.com/spf13/cobra v1.9.1 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
-	golang.org/x/sys v0.31.0 // indirect
+	github.com/spf13/viper v1.20.1 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
+	go.uber.org/atomic v1.11.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
 )

+ 29 - 0
go.sum

@@ -1,20 +1,49 @@
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60=
 github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
+github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 38 - 3
src/cmd/catv1/main.go

@@ -5,8 +5,10 @@
 package main
 
 import (
+	"github.com/SongZihuan/BackendServerTemplate/src/cmd/globalmain"
 	"github.com/SongZihuan/BackendServerTemplate/src/cmdparser/root"
 	_ "github.com/SongZihuan/BackendServerTemplate/src/global"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
 	catv1 "github.com/SongZihuan/BackendServerTemplate/src/mainfunc/cat/v1"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 	"github.com/spf13/cobra"
@@ -22,12 +24,16 @@ const (
 )
 
 func main() {
+	defer logger.Recover()
+
 	cmd := root.GetRootCMD("System service registration tool",
 		"Register this software as a system service, mainly used in Windows.",
+		nil,
+		false,
 		catv1.MainV1)
 
 	cmd.Flags().StringVarP(&catv1.InputConfigFilePath, "config", "c", catv1.InputConfigFilePath, "the file path of the configuration file")
-	cmd.Flags().StringVarP(&catv1.InputConfigFilePath, "output-config", "o", catv1.InputConfigFilePath, "the file path of the output configuration file")
+	cmd.Flags().StringVarP(&catv1.OutputConfigFilePath, "output-config", "o", catv1.InputConfigFilePath, "the file path of the output configuration file")
 
 	install := &cobra.Command{
 		Use:           args1Install,
@@ -43,6 +49,12 @@ func main() {
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
 			cmd.SilenceErrors = true
+
+			err := globalmain.PreRun(false)
+			if err != nil {
+				return err
+			}
+
 			return catv1.MainV1Install(cmd, args)
 		},
 		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
@@ -67,6 +79,12 @@ func main() {
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
 			cmd.SilenceErrors = true
+
+			err := globalmain.PreRun(false)
+			if err != nil {
+				return err
+			}
+
 			return catv1.MainV1UnInstall(cmd, args)
 		},
 		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
@@ -90,6 +108,12 @@ func main() {
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
 			cmd.SilenceErrors = true
+
+			err := globalmain.PreRun(false)
+			if err != nil {
+				return err
+			}
+
 			return catv1.MainV1Start(cmd, args)
 		},
 		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
@@ -113,6 +137,12 @@ func main() {
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
 			cmd.SilenceErrors = true
+
+			err := globalmain.PreRun(false)
+			if err != nil {
+				return err
+			}
+
 			return catv1.MainV1Stop(cmd, args)
 		},
 		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
@@ -136,6 +166,12 @@ func main() {
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
 			cmd.SilenceErrors = true
+
+			err := globalmain.PreRun(false)
+			if err != nil {
+				return err
+			}
+
 			return catv1.MainV1Restart(cmd, args)
 		},
 		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
@@ -146,6 +182,5 @@ func main() {
 	}
 
 	cmd.AddCommand(install, uninstall, start, stop, restart)
-
-	exitutils.Exit(cmd.Execute())
+	exitutils.ExitQuite(cmd.Execute())
 }

+ 11 - 5
src/cmd/globalmain/main.go

@@ -11,12 +11,19 @@ import (
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 )
 
-func PreRun() (exitCode error) {
+func PreRun(hasConsole bool) (exitCode error) {
 	var err error
 
-	err = consoleutils.SetConsoleCPSafe(consoleutils.CodePageUTF8)
-	if err != nil {
-		return exitutils.InitFailedErrorForWin32ConsoleModule(err.Error())
+	if hasConsole {
+		err = consoleutils.SetConsoleCPSafe(consoleutils.CodePageUTF8)
+		if err != nil {
+			return exitutils.InitFailedErrorForWin32ConsoleModule(err.Error())
+		}
+
+		err = consoleutils.BindStdToConsole()
+		if err != nil {
+			return exitutils.InitFailedErrorForWin32ConsoleModule(err.Error())
+		}
 	}
 
 	err = logger.InitBaseLogger(loglevel.LevelDebug, true, nil, nil, nil, nil)
@@ -29,5 +36,4 @@ func PreRun() (exitCode error) {
 
 func PostRun() {
 	defer logger.CloseLogger()
-	defer logger.Recover()
 }

+ 7 - 2
src/cmd/lionv1/main.go

@@ -7,17 +7,22 @@ package main
 import (
 	"github.com/SongZihuan/BackendServerTemplate/src/cmdparser/root"
 	_ "github.com/SongZihuan/BackendServerTemplate/src/global"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
 	lionv1 "github.com/SongZihuan/BackendServerTemplate/src/mainfunc/lion/v1"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 )
 
 func main() {
+	defer logger.Recover()
+
 	cmd := root.GetRootCMD("Multi-tasking background system",
 		"A multi-task background system controlled by a controller to run multiple tasks concurrently",
+		&lionv1.AutoReload,
+		true,
 		lionv1.MainV1)
 
 	cmd.Flags().StringVarP(&lionv1.InputConfigFilePath, "config", "c", lionv1.InputConfigFilePath, "the file path of the configuration file")
-	cmd.Flags().StringVarP(&lionv1.InputConfigFilePath, "output-config", "o", lionv1.InputConfigFilePath, "the file path of the output configuration file")
+	cmd.Flags().StringVarP(&lionv1.OutputConfigFilePath, "output-config", "o", lionv1.InputConfigFilePath, "the file path of the output configuration file")
 
-	exitutils.Exit(cmd.Execute())
+	exitutils.ExitQuite(cmd.Execute())
 }

+ 73 - 0
src/cmd/restart/posix.go

@@ -0,0 +1,73 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+//go:build !windows
+
+package restart
+
+import (
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/osutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/sliceutils"
+	"os"
+	"os/exec"
+	"time"
+)
+
+var RestartChan = make(chan bool)
+
+const RestartReadyTime = 5 * time.Second
+const RestartExitTime = 5 * time.Second
+const RestartWaitTime = RestartReadyTime + RestartExitTime + (3 * time.Second)
+
+func RestartProgram(restartFlag string, beforeReturnHook func()) error {
+	select {
+	case _, ok := <-RestartChan:
+		if ok == false {
+			return nil
+		}
+	default:
+		// pass
+	}
+
+	var args []string
+
+	if len(os.Args) > 1 {
+		args = sliceutils.CopySlice(os.Args[1:])
+		if !sliceutils.SliceHasItem(args, restartFlag) {
+			args = append([]string{restartFlag}, args...)
+		}
+	} else {
+		args = []string{restartFlag}
+	}
+
+	cmd := exec.Command(osutils.GetArgs0(), args...)
+	cmd.Stdin = os.Stdin
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+
+	// 保留默认设置,父进程结束后子进程将有init接管
+	//cmd.SysProcAttr = &syscall.SysProcAttr{
+	//	Setpgid: true, // 确保新的进程组独立
+	//}
+
+	if err := cmd.Start(); err != nil {
+		return err
+	}
+
+	if beforeReturnHook != nil {
+		beforeReturnHook()
+	}
+
+	close(RestartChan)
+	return nil
+}
+
+func FromRestart() error {
+	<-time.After(RestartWaitTime)
+	return nil
+}
+
+func FirstRun() error {
+	return nil
+}

+ 82 - 0
src/cmd/restart/win32.go

@@ -0,0 +1,82 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+//go:build windows
+
+package restart
+
+import (
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/consoleutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/osutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/sliceutils"
+	"golang.org/x/sys/windows"
+	"os"
+	"os/exec"
+	"syscall"
+	"time"
+)
+
+var RestartChan = make(chan bool)
+
+const RestartWaitTime = 5 * time.Second
+
+func RestartProgram(restartFlag string) error {
+	select {
+	case _, ok := <-RestartChan:
+		if ok == false {
+			return nil // 已经关闭
+		}
+	default:
+		// pass
+	}
+
+	var args []string
+
+	if len(os.Args) > 1 {
+		args = sliceutils.CopySlice(os.Args[1:])
+		if !sliceutils.SliceHasItem(args, restartFlag) {
+			args = append([]string{restartFlag}, args...)
+		}
+	} else {
+		args = []string{restartFlag}
+	}
+
+	cmd := exec.Command(osutils.GetArgs0(), args...)
+
+	cmd.SysProcAttr = &syscall.SysProcAttr{
+		CreationFlags: windows.CREATE_NEW_PROCESS_GROUP | windows.CREATE_UNICODE_ENVIRONMENT | windows.CREATE_NEW_CONSOLE,
+	}
+
+	if err := cmd.Start(); err != nil {
+		return err
+	}
+
+	logger.Warnf("the program restart")
+
+	if err := consoleutils.FreeConsole(); err != nil {
+		logger.Infof("free console error: %s", err.Error())
+		return err
+	}
+
+	logger.Infof("restart ready...")
+
+	close(RestartChan)
+	return nil
+}
+
+func FromRestart() error {
+	logger.Infof("Wait ready...")
+	<-time.After(RestartWaitTime)
+	logger.Infof("Restart ready...")
+	return nil
+}
+
+func FirstRun() error {
+	err := consoleutils.MakeNewConsole(consoleutils.CodePageUTF8)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 7 - 2
src/cmd/tigerv1/main.go

@@ -7,17 +7,22 @@ package main
 import (
 	"github.com/SongZihuan/BackendServerTemplate/src/cmdparser/root"
 	_ "github.com/SongZihuan/BackendServerTemplate/src/global"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
 	tigerv1 "github.com/SongZihuan/BackendServerTemplate/src/mainfunc/tiger/v1"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 )
 
 func main() {
+	defer logger.Recover()
+
 	cmd := root.GetRootCMD("Single-tasking background system",
 		"A single-task background system that runs a single task directly without using a controller",
+		&tigerv1.AutoReload,
+		true,
 		tigerv1.MainV1)
 
 	cmd.Flags().StringVarP(&tigerv1.InputConfigFilePath, "config", "c", tigerv1.InputConfigFilePath, "the file path of the configuration file")
-	cmd.Flags().StringVarP(&tigerv1.InputConfigFilePath, "output-config", "o", tigerv1.InputConfigFilePath, "the file path of the output configuration file")
+	cmd.Flags().StringVarP(&tigerv1.OutputConfigFilePath, "output-config", "o", tigerv1.InputConfigFilePath, "the file path of the output configuration file")
 
-	exitutils.Exit(cmd.Execute())
+	exitutils.ExitQuite(cmd.Execute())
 }

+ 93 - 2
src/cmdparser/root/root.go

@@ -5,18 +5,35 @@
 package root
 
 import (
+	"fmt"
+	"github.com/SongZihuan/BackendServerTemplate/src/cmd/globalmain"
+	"github.com/SongZihuan/BackendServerTemplate/src/cmd/restart"
 	"github.com/SongZihuan/BackendServerTemplate/src/cmdparser/license"
 	"github.com/SongZihuan/BackendServerTemplate/src/cmdparser/report"
 	"github.com/SongZihuan/BackendServerTemplate/src/cmdparser/version"
 	"github.com/SongZihuan/BackendServerTemplate/src/global"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/cleanstringutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/consoleutils"
 	"github.com/spf13/cobra"
 )
 
+const (
+	ConsoleModeNormal = "normal"
+	ConsoleModeNO     = "no"
+)
+
 var name string = global.Name
-var nameChanged bool
+var nameChanged bool = false
+var isRestart bool = false
+var consoleMode string = ConsoleModeNormal
+var hasConsole bool = false
+
+const restartFlag = "restart"
+const RestartFlag = "--" + restartFlag
+
+func GetRootCMD(shortDescribe string, longDescribe string, reload *bool, _hasConsole bool, action func(cmd *cobra.Command, args []string) error) *cobra.Command {
+	hasConsole = _hasConsole && consoleutils.HasConsoleWindow()
 
-func GetRootCMD(shortDescribe string, longDescribe string, action func(cmd *cobra.Command, args []string) error) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:           global.Name,
 		Short:         shortDescribe,
@@ -33,13 +50,59 @@ func GetRootCMD(shortDescribe string, longDescribe string, action func(cmd *cobr
 			} else {
 				nameChanged = false
 			}
+
+			return nil
+		},
+		PreRunE: func(cmd *cobra.Command, args []string) error {
+			cmd.SilenceUsage = false
+			cmd.SilenceErrors = false
+
+			if hasConsole {
+				if consoleMode != ConsoleModeNormal && consoleMode != ConsoleModeNO {
+					return fmt.Errorf("console-mode must be %s or %s", ConsoleModeNormal, ConsoleModeNO)
+				}
+			} else if _hasConsole && consoleMode != ConsoleModeNO {
+				return fmt.Errorf("console-mode must be %s, there is not console be found", ConsoleModeNO)
+			}
+
+			if reload != nil && *reload {
+				if consoleMode == ConsoleModeNO {
+					return fmt.Errorf("`auto-reload` can only be enabled when `console-mode` is `normal`")
+				}
+
+				if cmd.Flags().Changed(restartFlag) {
+					if isRestart {
+						err := restart.FromRestart()
+						if err != nil {
+							return fmt.Errorf("restart failed to attach console: %s", err)
+						}
+					} else {
+						return fmt.Errorf("`restart` cannot be specified as false")
+					}
+				} else {
+					return restart.FirstRun()
+				}
+			}
+
 			return nil
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = true
 			cmd.SilenceErrors = true
+
+			err := globalmain.PreRun(HasConsole())
+			if err != nil {
+				return err
+			}
+
 			return action(cmd, args)
 		},
+		PostRunE: func(cmd *cobra.Command, args []string) error {
+			cmd.SilenceUsage = false
+			cmd.SilenceErrors = false
+			globalmain.PostRun()
+			return nil
+		},
 		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
 			cmd.SilenceUsage = false
 			cmd.SilenceErrors = false
@@ -50,6 +113,15 @@ func GetRootCMD(shortDescribe string, longDescribe string, action func(cmd *cobr
 	cmd.AddCommand(version.CMD, license.CMD, report.CMD)
 	cmd.PersistentFlags().StringVarP(&name, "name", "n", global.Name, "the program display name")
 
+	if _hasConsole {
+		cmd.Flags().StringVar(&consoleMode, "console-mode", ConsoleModeNormal, "the console mode. normally, select `normal`. If you are not using a terminal (e.g. redirection, etc.) please select `no`.")
+	}
+
+	if reload != nil {
+		cmd.Flags().BoolVar(reload, "auto-reload", false, "auto reload config file when the file changed")
+		cmd.Flags().BoolVar(&isRestart, restartFlag, false, "restart mode, note: DO NOT SET THIS FLAG unless you know your purpose clearly.")
+	}
+
 	return cmd
 }
 
@@ -60,3 +132,22 @@ func Name() string {
 func NameChanged() bool {
 	return nameChanged
 }
+
+func IsRestart() bool {
+	return isRestart
+}
+
+func ConsoleMode() string {
+	switch consoleMode {
+	case ConsoleModeNormal:
+		return ConsoleModeNormal
+	case ConsoleModeNO:
+		return ConsoleModeNO
+	default:
+		return ConsoleModeNormal
+	}
+}
+
+func HasConsole() bool {
+	return hasConsole && ConsoleMode() == ConsoleModeNormal
+}

+ 5 - 5
src/config/base_data.go

@@ -10,11 +10,11 @@ import (
 )
 
 type ConfigData struct {
-	GlobalConfig `json:",inline" yaml:",inline"`
-	Logger       LoggerConfig       `json:"logger" yaml:"logger"`
-	Signal       SignalConfig       `json:"signal" yaml:"signal"`
-	Win32Console Win32ConsoleConfig `json:"win32-console" yaml:"win32-console"`
-	Server       ServerConfig       `json:"server" yaml:"server"`
+	GlobalConfig `json:",inline" yaml:",inline" mapstructure:",squash"`
+	Logger       LoggerConfig       `json:"logger" yaml:"logger" mapstructure:"logger"`
+	Signal       SignalConfig       `json:"signal" yaml:"signal" mapstructure:"signal"`
+	Win32Console Win32ConsoleConfig `json:"win32-console" yaml:"win32-console" mapstructure:"win32-console"`
+	Server       ServerConfig       `json:"server" yaml:"server" mapstructure:"server"`
 }
 
 func (d *ConfigData) init(filePath string, provider configparser.ConfigParserProvider) (err configerror.Error) {

+ 1 - 1
src/config/config.go

@@ -31,7 +31,7 @@ func newConfig(inputFilePath string, outputFilePath string, provider configparse
 	}
 
 	if provider == nil {
-		provider = configparser.NewYamlProvider()
+		provider = configparser.NewYamlProvider(nil)
 	}
 
 	if !provider.CanUTF8() {

+ 58 - 17
src/config/configparser/json.go

@@ -6,22 +6,59 @@ package configparser
 
 import (
 	"encoding/json"
-	"fmt"
+	"errors"
+	"github.com/SongZihuan/BackendServerTemplate/src/cmd/restart"
+	"github.com/SongZihuan/BackendServerTemplate/src/cmdparser/root"
 	"github.com/SongZihuan/BackendServerTemplate/src/config/configerror"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/envutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/osutils"
+	"github.com/fsnotify/fsnotify"
+	"github.com/spf13/viper"
 	"os"
 	"reflect"
 )
 
 type JsonProvider struct {
-	HasRead  bool
-	FileData []byte
+	viper   *viper.Viper
+	hasRead bool
 }
 
-func NewJsonProvider() *JsonProvider {
-	return &JsonProvider{
-		HasRead:  false,
-		FileData: nil,
+func NewJsonProvider(opt *NewProviderOption) *JsonProvider {
+	if opt == nil {
+		opt = new(NewProviderOption)
 	}
+
+	if opt.EnvPrefix == "" {
+		opt.EnvPrefix = envutils.StringToEnvName(osutils.GetArgs0NamePOSIX())
+	}
+
+	p := &JsonProvider{
+		viper:   viper.New(),
+		hasRead: false,
+	}
+
+	// 环境变量
+	p.viper.SetEnvPrefix(opt.EnvPrefix)
+	p.viper.SetEnvKeyReplacer(envutils.GetEnvReplaced())
+	p.viper.AutomaticEnv()
+
+	if opt.AutoReload {
+		logger.Infof("start auto reload")
+
+		p.viper.OnConfigChange(func(e fsnotify.Event) {
+			logger.Infof("config change")
+
+			err := restart.RestartProgram(root.RestartFlag)
+			if err != nil {
+				logger.Errorf("restart program error: %s", err.Error())
+			}
+		})
+
+		p.viper.WatchConfig()
+	}
+
+	return p
 }
 
 func (j *JsonProvider) CanUTF8() bool {
@@ -29,23 +66,27 @@ func (j *JsonProvider) CanUTF8() bool {
 }
 
 func (j *JsonProvider) ReadFile(filepath string) configerror.Error {
-	if j.HasRead {
+	if j.hasRead {
 		return configerror.NewErrorf("config file has been read")
 	}
 
-	data, err := os.ReadFile(filepath)
+	j.viper.SetConfigFile(filepath)
+	j.viper.SetConfigType("json")
+	err := j.viper.ReadInConfig()
 	if err != nil {
-		return configerror.NewErrorf(fmt.Sprintf("read file error: %s", err.Error()))
+		if errors.Is(err, viper.ConfigFileNotFoundError{}) {
+			return configerror.NewErrorf("config file not found: %s", err.Error())
+		}
+		return configerror.NewErrorf("read config file error: %s", err.Error())
 	}
 
-	j.FileData = data
-	j.HasRead = true
+	j.hasRead = true
 
 	return nil
 }
 
 func (j *JsonProvider) ParserFile(target any) configerror.Error {
-	if !j.HasRead || j.FileData == nil {
+	if !j.hasRead {
 		return configerror.NewErrorf("config file has not been read")
 	}
 
@@ -53,16 +94,16 @@ func (j *JsonProvider) ParserFile(target any) configerror.Error {
 		return configerror.NewErrorf("target must be a pointer")
 	}
 
-	err := json.Unmarshal(j.FileData, target)
+	err := j.viper.Unmarshal(target)
 	if err != nil {
-		return configerror.NewErrorf("json parser error: %s", err.Error())
+		return configerror.NewErrorf("yaml unmarshal error: %s", err.Error())
 	}
 
 	return nil
 }
 
 func (j *JsonProvider) WriteFile(filepath string, src any) configerror.Error {
-	if !j.HasRead {
+	if !j.hasRead {
 		return configerror.NewErrorf("config file has not been read")
 	}
 
@@ -70,7 +111,7 @@ func (j *JsonProvider) WriteFile(filepath string, src any) configerror.Error {
 		return configerror.NewErrorf("target must be a pointer")
 	}
 
-	target, err := json.Marshal(src)
+	target, err := json.MarshalIndent(src, "", "  ")
 	if err != nil {
 		return configerror.NewErrorf("json marshal error: %s", err.Error())
 	}

+ 5 - 0
src/config/configparser/parser.go

@@ -12,3 +12,8 @@ type ConfigParserProvider interface {
 	ParserFile(target any) configerror.Error
 	WriteFile(filepath string, data any) configerror.Error
 }
+
+type NewProviderOption struct {
+	EnvPrefix  string
+	AutoReload bool
+}

+ 21 - 0
src/config/configparser/provider.go

@@ -0,0 +1,21 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package configparser
+
+import (
+	"fmt"
+	"strings"
+)
+
+func NewProvider(configPath string, opt *NewProviderOption) (ConfigParserProvider, error) {
+	switch {
+	case strings.HasSuffix(configPath, ".yaml") || strings.HasSuffix(configPath, ".yml"):
+		return NewYamlProvider(opt), nil
+	case strings.HasSuffix(configPath, ".yml"):
+		return NewJsonProvider(opt), nil
+	default:
+		return nil, fmt.Errorf("config file type unknown")
+	}
+}

+ 73 - 15
src/config/configparser/yaml.go

@@ -5,23 +5,72 @@
 package configparser
 
 import (
-	"fmt"
+	"errors"
+	"github.com/SongZihuan/BackendServerTemplate/src/cmd/restart"
+	"github.com/SongZihuan/BackendServerTemplate/src/cmdparser/root"
 	"github.com/SongZihuan/BackendServerTemplate/src/config/configerror"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/envutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/osutils"
+	"github.com/fsnotify/fsnotify"
+	"github.com/spf13/viper"
 	"gopkg.in/yaml.v3"
 	"os"
 	"reflect"
+	"sync"
 )
 
 type YamlProvider struct {
-	HasRead  bool
-	FileData []byte
+	viper      *viper.Viper
+	autoReload bool
+	reloadLock sync.Mutex
+	hasRead    bool
 }
 
-func NewYamlProvider() *YamlProvider {
-	return &YamlProvider{
-		HasRead:  false,
-		FileData: nil,
+func NewYamlProvider(opt *NewProviderOption) *YamlProvider {
+	if opt == nil {
+		opt = new(NewProviderOption)
 	}
+
+	if opt.EnvPrefix == "" {
+		opt.EnvPrefix = envutils.StringToEnvName(osutils.GetArgs0NamePOSIX())
+	}
+
+	p := &YamlProvider{
+		viper:   viper.New(),
+		hasRead: false,
+	}
+
+	// 环境变量
+	p.viper.SetEnvPrefix(opt.EnvPrefix)
+	p.viper.SetEnvKeyReplacer(envutils.GetEnvReplaced())
+	p.viper.AutomaticEnv()
+
+	if opt.AutoReload {
+		logger.Infof("start auto reload")
+		p.viper.OnConfigChange(p.reloadEvent)
+		p.autoReload = true
+	} else {
+		p.autoReload = false
+	}
+
+	return p
+}
+
+func (y *YamlProvider) reloadEvent(e fsnotify.Event) {
+	if ok := y.reloadLock.TryLock(); !ok {
+		return
+	}
+
+	logger.Infof("config change")
+	err := restart.RestartProgram(root.RestartFlag)
+	if err != nil {
+		logger.Errorf("restart program error: %s", err.Error())
+		y.reloadLock.Unlock()
+		return
+	}
+
+	// 不需要释放 y.reloadLock 锁
 }
 
 func (y *YamlProvider) CanUTF8() bool {
@@ -29,23 +78,32 @@ func (y *YamlProvider) CanUTF8() bool {
 }
 
 func (y *YamlProvider) ReadFile(filepath string) configerror.Error {
-	if y.HasRead {
+	if y.hasRead {
 		return configerror.NewErrorf("config file has been read")
 	}
 
-	data, err := os.ReadFile(filepath)
+	y.viper.SetConfigFile(filepath)
+	y.viper.SetConfigType("yaml")
+	err := y.viper.ReadInConfig()
 	if err != nil {
-		return configerror.NewErrorf(fmt.Sprintf("read file error: %s", err.Error()))
+		if errors.Is(err, viper.ConfigFileNotFoundError{}) {
+			return configerror.NewErrorf("config file not found: %s", err.Error())
+		}
+		return configerror.NewErrorf("read config file error: %s", err.Error())
+	}
+
+	if y.autoReload {
+		logger.Infof("auto reload: watch file: %s", y.viper.ConfigFileUsed())
+		y.viper.WatchConfig()
 	}
 
-	y.FileData = data
-	y.HasRead = true
+	y.hasRead = true
 
 	return nil
 }
 
 func (y *YamlProvider) ParserFile(target any) configerror.Error {
-	if !y.HasRead {
+	if !y.hasRead {
 		return configerror.NewErrorf("config file has not been read")
 	}
 
@@ -53,7 +111,7 @@ func (y *YamlProvider) ParserFile(target any) configerror.Error {
 		return configerror.NewErrorf("target must be a pointer")
 	}
 
-	err := yaml.Unmarshal(y.FileData, target)
+	err := y.viper.Unmarshal(target)
 	if err != nil {
 		return configerror.NewErrorf("yaml unmarshal error: %s", err.Error())
 	}
@@ -62,7 +120,7 @@ func (y *YamlProvider) ParserFile(target any) configerror.Error {
 }
 
 func (y *YamlProvider) WriteFile(filepath string, src any) configerror.Error {
-	if !y.HasRead {
+	if !y.hasRead {
 		return configerror.NewErrorf("config file has not been read")
 	}
 

+ 5 - 8
src/config/export.go

@@ -11,7 +11,6 @@ import (
 	"github.com/SongZihuan/BackendServerTemplate/src/logger"
 	"os"
 	"path"
-	"strings"
 )
 
 var config *configInfo
@@ -31,16 +30,14 @@ func (opt *ConfigOption) setDefault() error {
 		}
 
 		opt.ConfigFilePath = path.Join(wd, "config.yaml")
-		opt.Provider = configparser.NewYamlProvider()
 	}
 
 	if opt.Provider == nil {
-		if strings.HasSuffix(opt.ConfigFilePath, ".yaml") || strings.HasSuffix(opt.ConfigFilePath, ".yml") {
-			opt.Provider = configparser.NewYamlProvider()
-		} else if strings.HasSuffix(opt.ConfigFilePath, ".json") {
-			opt.Provider = configparser.NewJsonProvider()
-		} else {
-			opt.Provider = configparser.NewYamlProvider()
+		var err error
+
+		opt.Provider, err = configparser.NewProvider(opt.ConfigFilePath, nil)
+		if err != nil {
+			return err
 		}
 	}
 

+ 6 - 6
src/config/global_config.go

@@ -23,14 +23,14 @@ const (
 )
 
 type GlobalConfig struct {
-	Name     string  `json:"name" yaml:"name"`
-	Mode     RunMode `json:"mode" yaml:"mode"`
-	Timezone string  `json:"time-zone" yaml:"time-zone"`
+	Name     string  `json:"name" yaml:"name" mapstructure:"name"`
+	Mode     RunMode `json:"mode" yaml:"mode" mapstructure:"mode"`
+	Timezone string  `json:"time-zone" yaml:"time-zone" mapstructure:"time-zone"`
 
 	// Time UTCDate Timestamp 记录为配置文件读取时间
-	Time      time.Time `json:"-" yaml:"-"`
-	UTCDate   string    `json:"utc-date" yaml:"utc-date"`
-	Timestamp int64     `json:"timestamp" yaml:"timestamp"`
+	Time      time.Time `json:"-" yaml:"-" mapstructure:"-"`
+	UTCDate   string    `json:"utc-date" yaml:"utc-date" mapstructure:"utc-date"`
+	Timestamp int64     `json:"timestamp" yaml:"timestamp" mapstructure:"timestamp"`
 }
 
 func (d *GlobalConfig) init(filePath string, provider configparser.ConfigParserProvider) configerror.Error {

+ 6 - 6
src/config/logger_config.go

@@ -13,13 +13,13 @@ import (
 )
 
 type LoggerConfig struct {
-	LogLevel loglevel.LoggerLevel `json:"log-level" yaml:"log-level"`
-	LogTag   typeutils.StringBool `json:"log-tag" yaml:"log-tag"`
+	LogLevel loglevel.LoggerLevel `json:"log-level" yaml:"log-level" mapstructure:"log-level"`
+	LogTag   typeutils.StringBool `json:"log-tag" yaml:"log-tag" mapstructure:"log-tag"`
 
-	HumanWarnWriter   LoggerWriterConfig `json:"human-warn-writer" yaml:"human-warn-writer"`
-	HumanErrWriter    LoggerWriterConfig `json:"human-error-writer" yaml:"human-error-writer"`
-	MachineWarnWriter LoggerWriterConfig `json:"machine-warn-writer" yaml:"machine-warn-writer"`
-	MachineErrWriter  LoggerWriterConfig `json:"machine-error-writer" yaml:"machine-error-writer"`
+	HumanWarnWriter   LoggerWriterConfig `json:"human-warn-writer" yaml:"human-warn-writer" mapstructure:"human-warn-writer"`
+	HumanErrWriter    LoggerWriterConfig `json:"human-error-writer" yaml:"human-error-writer" mapstructure:"human-error-writer"`
+	MachineWarnWriter LoggerWriterConfig `json:"machine-warn-writer" yaml:"machine-warn-writer" mapstructure:"machine-warn-writer"`
+	MachineErrWriter  LoggerWriterConfig `json:"machine-error-writer" yaml:"machine-error-writer" mapstructure:"machine-error-writer"`
 }
 
 func (d *LoggerConfig) init(filePath string, provider configparser.ConfigParserProvider) (err configerror.Error) {

+ 4 - 4
src/config/logger_writer_config.go

@@ -20,10 +20,10 @@ import (
 )
 
 type LoggerWriterConfig struct {
-	WriteToStd          string `json:"write-to-std" yaml:"write-to-std"` // stdout stderr all no
-	WriteToFile         string `json:"write-to-file" yaml:"write-to-file"`
-	WriteToDirWithDate  string `json:"write-to-dir-with-date" yaml:"write-to-dir-with-date"`
-	WriteWithDatePrefix string `json:"write-with-date-prefix" yaml:"write-with-date-prefix"`
+	WriteToStd          string `json:"write-to-std" yaml:"write-to-std" mapstructure:"write-to-std"` // stdout stderr all no
+	WriteToFile         string `json:"write-to-file" yaml:"write-to-file" mapstructure:"write-to-file"`
+	WriteToDirWithDate  string `json:"write-to-dir-with-date" yaml:"write-to-dir-with-date" mapstructure:"write-to-dir-with-date"`
+	WriteWithDatePrefix string `json:"write-with-date-prefix" yaml:"write-with-date-prefix" mapstructure:"write-with-date-prefix"`
 }
 
 func (d *LoggerWriterConfig) init(filePath string, provider configparser.ConfigParserProvider) (err configerror.Error) {

+ 6 - 6
src/config/signal_config.go → src/config/posix_signal_config.go

@@ -13,12 +13,12 @@ import (
 )
 
 type SignalConfig struct {
-	UseOn       string               `json:"use-on" yaml:"use-on"`
-	Use         bool                 `json:"-" yaml:"-"`
-	SigIntExit  typeutils.StringBool `json:"sigint-exit" yaml:"sigint-exit"`
-	SigTermExit typeutils.StringBool `json:"sigterm-exit" yaml:"sigterm-exit"`
-	SigHupExit  typeutils.StringBool `json:"sighup-exit" yaml:"sighup-exit"`
-	SigQuitExit typeutils.StringBool `json:"sigquit-exit" yaml:"sigquit-exit"`
+	UseOn       string               `json:"use-on" yaml:"use-on" mapstructure:"use-on"`
+	Use         bool                 `json:"-" yaml:"-" mapstructure:"-"`
+	SigIntExit  typeutils.StringBool `json:"sigint-exit" yaml:"sigint-exit" mapstructure:"sigint-exit"`
+	SigTermExit typeutils.StringBool `json:"sigterm-exit" yaml:"sigterm-exit" mapstructure:"sigterm-exit"`
+	SigHupExit  typeutils.StringBool `json:"sighup-exit" yaml:"sighup-exit" mapstructure:"sighup-exit"`
+	SigQuitExit typeutils.StringBool `json:"sigquit-exit" yaml:"sigquit-exit" mapstructure:"sigquit-exit"`
 }
 
 func (d *SignalConfig) init(filePath string, provider configparser.ConfigParserProvider) (err configerror.Error) {

+ 7 - 2
src/config/server_config.go

@@ -12,9 +12,10 @@ import (
 )
 
 type ServerConfig struct {
-	StopWaitTime string `json:"stop-wait-time" yaml:"stop-wait-time"`
+	StopWaitTime string `json:"stop-wait-time" yaml:"stop-wait-time" mapstructure:"stop-wait-time"`
+	Name         string `json:"name" yaml:"name" mapstructure:"name"`
 
-	StopWaitTimeDuration time.Duration `yaml:"-"`
+	StopWaitTimeDuration time.Duration `json:"-" yaml:"-" mapstructure:"-"`
 }
 
 func (d *ServerConfig) init(filePath string, provider configparser.ConfigParserProvider) (err configerror.Error) {
@@ -25,6 +26,10 @@ func (d *ServerConfig) setDefault(c *configInfo) (err configerror.Error) {
 	if d.StopWaitTime == "" {
 		d.StopWaitTime = "10s"
 	}
+
+	if d.Name == "" {
+		d.Name = "Jack"
+	}
 	return nil
 }
 

+ 5 - 5
src/config/win32_consolel_config.go

@@ -13,11 +13,11 @@ import (
 )
 
 type Win32ConsoleConfig struct {
-	UseOn                string               `json:"use-on" yaml:"use-on"`
-	Use                  bool                 `json:"-" yaml:"-"`
-	CtrlCExit            typeutils.StringBool `json:"ctrl-c-exit" yaml:"ctrl-c-exit"`
-	CtrlBreakExit        typeutils.StringBool `json:"ctrl-break-exit" yaml:"ctrl-break-exit"`
-	ConsoleCloseRecovery typeutils.StringBool `json:"console-close-recovery" yaml:"console-close-recovery"`
+	UseOn                string               `json:"use-on" yaml:"use-on" mapstructure:"use-on"`
+	Use                  bool                 `json:"-" yaml:"-" mapstructure:"-"`
+	CtrlCExit            typeutils.StringBool `json:"ctrl-c-exit" yaml:"ctrl-c-exit" mapstructure:"ctrl-c-exit"`
+	CtrlBreakExit        typeutils.StringBool `json:"ctrl-break-exit" yaml:"ctrl-break-exit" mapstructure:"ctrl-break-exit"`
+	ConsoleCloseRecovery typeutils.StringBool `json:"console-close-recovery" yaml:"console-close-recovery" mapstructure:"console-close-recovery"`
 }
 
 func (d *Win32ConsoleConfig) init(filePath string, provider configparser.ConfigParserProvider) (err configerror.Error) {

+ 3 - 3
src/consolewatcher/win32.go

@@ -22,15 +22,15 @@ func consoleHandler(exitChannel chan consoleutils.Event, waitExitChannel chan an
 			exitChannel <- consoleutils.EventMap[event]
 
 			if config.Data().Win32Console.ConsoleCloseRecovery.IsEnable(false) {
+				logger.Warnf("终端暂时重启,等待程序清理完毕,请勿关闭当前终端!")
+				logger.Warnf("若不希望重启终端,可在配置文件处关闭。")
+
 				err := consoleutils.MakeNewConsole(consoleutils.CodePageUTF8)
 				if err != nil {
 					logger.Errorf("win32 make new console failed: %s", err.Error())
 				}
 			}
 
-			logger.Warnf("终端暂时重启,等待程序清理完毕,请勿关闭当前终端!")
-			logger.Warnf("若不希望重启终端,可在配置文件处关闭。")
-
 			select {
 			case <-waitExitChannel:
 				// pass

+ 6 - 34
src/mainfunc/cat/v1/main.go

@@ -5,20 +5,22 @@
 package v1
 
 import (
-	"github.com/SongZihuan/BackendServerTemplate/src/cmd/globalmain"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 	"github.com/kardianos/service"
 	"github.com/spf13/cobra"
+	"os"
 )
 
 func MainV1(cmd *cobra.Command, args []string) (exitCode error) {
 	var err error
 
-	err = globalmain.PreRun()
+	logfile, err := os.OpenFile("C:\\Users\\songz\\Code\\GoProject\\BackendServerTemplate\\test_self\\tmpcat.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
 	if err != nil {
-		return err
+		panic(err)
 	}
-	defer globalmain.PostRun()
+	defer func() {
+		_ = logfile.Close()
+	}()
 
 	err = initServiceConfig()
 	if err != nil {
@@ -47,12 +49,6 @@ func MainV1(cmd *cobra.Command, args []string) (exitCode error) {
 func MainV1Install(cmd *cobra.Command, args []string) (exitCode error) {
 	var err error
 
-	err = globalmain.PreRun()
-	if err != nil {
-		return err
-	}
-	defer globalmain.PostRun()
-
 	err = initInstallServiceConfig(args)
 	if err != nil {
 		return exitutils.InitFailedError("service config", err.Error())
@@ -85,12 +81,6 @@ func MainV1Install(cmd *cobra.Command, args []string) (exitCode error) {
 func MainV1UnInstall(cmd *cobra.Command, args []string) (exitCode error) {
 	var err error
 
-	err = globalmain.PreRun()
-	if err != nil {
-		return err
-	}
-	defer globalmain.PostRun()
-
 	err = initServiceConfig()
 	if err != nil {
 		return exitutils.InitFailedError("service config", err.Error())
@@ -123,12 +113,6 @@ func MainV1UnInstall(cmd *cobra.Command, args []string) (exitCode error) {
 func MainV1Start(cmd *cobra.Command, args []string) (exitCode error) {
 	var err error
 
-	err = globalmain.PreRun()
-	if err != nil {
-		return err
-	}
-	defer globalmain.PostRun()
-
 	err = initServiceConfig()
 	if err != nil {
 		return exitutils.InitFailedError("service config", err.Error())
@@ -161,12 +145,6 @@ func MainV1Start(cmd *cobra.Command, args []string) (exitCode error) {
 func MainV1Stop(cmd *cobra.Command, args []string) (exitCode error) {
 	var err error
 
-	err = globalmain.PreRun()
-	if err != nil {
-		return err
-	}
-	defer globalmain.PostRun()
-
 	err = initServiceConfig()
 	if err != nil {
 		return exitutils.InitFailedError("service config", err.Error())
@@ -199,12 +177,6 @@ func MainV1Stop(cmd *cobra.Command, args []string) (exitCode error) {
 func MainV1Restart(cmd *cobra.Command, args []string) (exitCode error) {
 	var err error
 
-	err = globalmain.PreRun()
-	if err != nil {
-		return err
-	}
-	defer globalmain.PostRun()
-
 	err = initServiceConfig()
 	if err != nil {
 		return exitutils.InitFailedError("service config", err.Error())

+ 14 - 25
src/mainfunc/cat/v1/service.go

@@ -8,13 +8,11 @@ import (
 	"errors"
 	"github.com/SongZihuan/BackendServerTemplate/src/config"
 	"github.com/SongZihuan/BackendServerTemplate/src/config/configparser"
-	"github.com/SongZihuan/BackendServerTemplate/src/consolewatcher"
 	"github.com/SongZihuan/BackendServerTemplate/src/logger"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/example3"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/servercontext"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/serverinterface"
 	"github.com/SongZihuan/BackendServerTemplate/src/signalwatcher"
-	"github.com/SongZihuan/BackendServerTemplate/src/utils/consoleutils"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 	"github.com/kardianos/service"
 	"os"
@@ -24,12 +22,10 @@ var InputConfigFilePath string = "config.yaml"
 var OutputConfigFilePath string = ""
 
 type Program struct {
-	sigchan             chan os.Signal
-	consolechan         chan consoleutils.Event
-	consolewaitexitchan chan any
-	stopErr             error
-	ser                 serverinterface.Server
-	exitCode            exitutils.ExitCode
+	sigchan  chan os.Signal
+	stopErr  error
+	ser      serverinterface.Server
+	exitCode exitutils.ExitCode
 }
 
 func NewProgram() *Program {
@@ -39,10 +35,16 @@ func NewProgram() *Program {
 func (p *Program) Start(s service.Service) error {
 	var err error
 
+	configProvider, err := configparser.NewProvider(InputConfigFilePath, nil)
+	if err != nil {
+		p.exitCode = exitutils.InitFailedError("Get config file provider", err.Error())
+		return err
+	}
+
 	err = config.InitConfig(&config.ConfigOption{
 		ConfigFilePath: InputConfigFilePath,
 		OutputFilePath: OutputConfigFilePath,
-		Provider:       configparser.NewYamlProvider(),
+		Provider:       configProvider,
 	})
 	if err != nil {
 		p.exitCode = exitutils.InitFailedError("Config file read and parser", err.Error())
@@ -51,12 +53,6 @@ func (p *Program) Start(s service.Service) error {
 
 	p.sigchan = signalwatcher.NewSignalExitChannel()
 
-	p.consolechan, p.consolewaitexitchan, err = consolewatcher.NewWin32ConsoleExitChannel()
-	if err != nil {
-		p.exitCode = exitutils.InitFailedError("Win32 console channel", err.Error())
-		return err
-	}
-
 	p.ser, _, err = example3.NewServerExample3(&example3.ServerExample3Option{
 		StopWaitTime: config.Data().Server.StopWaitTimeDuration,
 	})
@@ -69,21 +65,17 @@ func (p *Program) Start(s service.Service) error {
 	go func() {
 		select {
 		case sig := <-p.sigchan:
-			logger.Warnf("stop by signal (%s)", sig.String())
-			err = nil
-			p.stopErr = nil
-		case event := <-p.consolechan:
-			logger.Infof("stop by console event (%s)", event.String())
+			logger.Warnf("Stop by signal (%s)", sig.String())
 			err = nil
 			p.stopErr = nil
 		case <-p.ser.GetCtx().Listen():
 			err = p.ser.GetCtx().Error()
 			if err == nil || errors.Is(err, servercontext.StopAllTask) {
-				logger.Infof("stop by server")
+				logger.Infof("Stop by server")
 				err = nil
 				p.stopErr = nil
 			} else {
-				logger.Errorf("stop by server with error: %s", err.Error())
+				logger.Errorf("Stop by server with error: %s", err.Error())
 				p.stopErr = err
 			}
 		}
@@ -99,13 +91,10 @@ func (p *Program) Start(s service.Service) error {
 
 func (p *Program) Stop(s service.Service) error {
 	p.ser.Stop()
-	close(p.consolewaitexitchan)
-
 	if p.stopErr != nil {
 		p.exitCode = exitutils.RunError(p.stopErr.Error())
 		return p.stopErr
 	}
-
 	p.exitCode = exitutils.SuccessExit("all tasks are completed and the main go routine exits")
 	return nil
 }

+ 17 - 5
src/mainfunc/lion/v1/main.go

@@ -6,7 +6,7 @@ package v1
 
 import (
 	"errors"
-	"github.com/SongZihuan/BackendServerTemplate/src/cmd/globalmain"
+	"github.com/SongZihuan/BackendServerTemplate/src/cmd/restart"
 	"github.com/SongZihuan/BackendServerTemplate/src/config"
 	"github.com/SongZihuan/BackendServerTemplate/src/config/configparser"
 	"github.com/SongZihuan/BackendServerTemplate/src/consolewatcher"
@@ -22,20 +22,22 @@ import (
 
 var InputConfigFilePath string = "config.yaml"
 var OutputConfigFilePath string = ""
+var AutoReload bool = false
 
 func MainV1(cmd *cobra.Command, args []string) (exitCode error) {
 	var err error
 
-	err = globalmain.PreRun()
+	configProvider, err := configparser.NewProvider(InputConfigFilePath, &configparser.NewProviderOption{
+		AutoReload: AutoReload,
+	})
 	if err != nil {
-		return err
+		return exitutils.InitFailedError("Get config file provider", err.Error())
 	}
-	defer globalmain.PostRun()
 
 	err = config.InitConfig(&config.ConfigOption{
 		ConfigFilePath: InputConfigFilePath,
 		OutputFilePath: OutputConfigFilePath,
-		Provider:       configparser.NewYamlProvider(),
+		Provider:       configProvider,
 	})
 	if err != nil {
 		return exitutils.InitFailedError("Config file read and parser", err.Error())
@@ -79,7 +81,17 @@ func MainV1(cmd *cobra.Command, args []string) (exitCode error) {
 	go ctrl.Run()
 
 	var stopErr error
+
+SELECT:
 	select {
+	case <-restart.RestartChan:
+		if AutoReload {
+			logger.Warnf("stop/restart by config file change")
+			err = nil
+			stopErr = nil
+		} else {
+			goto SELECT
+		}
 	case sig := <-sigchan:
 		logger.Warnf("stop by signal (%s)", sig.String())
 		err = nil

+ 15 - 5
src/mainfunc/tiger/v1/main.go

@@ -6,7 +6,7 @@ package v1
 
 import (
 	"errors"
-	"github.com/SongZihuan/BackendServerTemplate/src/cmd/globalmain"
+	"github.com/SongZihuan/BackendServerTemplate/src/cmd/restart"
 	"github.com/SongZihuan/BackendServerTemplate/src/config"
 	"github.com/SongZihuan/BackendServerTemplate/src/config/configparser"
 	"github.com/SongZihuan/BackendServerTemplate/src/consolewatcher"
@@ -20,20 +20,20 @@ import (
 
 var InputConfigFilePath string = "config.yaml"
 var OutputConfigFilePath string = ""
+var AutoReload bool = false
 
 func MainV1(cmd *cobra.Command, args []string) (exitCode error) {
 	var err error
 
-	err = globalmain.PreRun()
+	configProvider, err := configparser.NewProvider(InputConfigFilePath, nil)
 	if err != nil {
-		return err
+		return exitutils.InitFailedError("Get config file provider", err.Error())
 	}
-	defer globalmain.PostRun()
 
 	err = config.InitConfig(&config.ConfigOption{
 		ConfigFilePath: InputConfigFilePath,
 		OutputFilePath: OutputConfigFilePath,
-		Provider:       configparser.NewYamlProvider(),
+		Provider:       configProvider,
 	})
 	if err != nil {
 		return exitutils.InitFailedError("Config file read and parser", err.Error())
@@ -57,7 +57,17 @@ func MainV1(cmd *cobra.Command, args []string) (exitCode error) {
 	go ser.Run()
 
 	var stopErr error
+
+SELECT:
 	select {
+	case <-restart.RestartChan:
+		if AutoReload {
+			logger.Warnf("stop/restart by config file change")
+			err = nil
+			stopErr = nil
+		} else {
+			goto SELECT
+		}
 	case sig := <-sigchan:
 		logger.Warnf("stop by signal (%s)", sig.String())
 		err = nil

+ 5 - 5
src/server/example1/server.go

@@ -79,11 +79,11 @@ func (s *ServerExample1) Run() {
 
 MainCycle:
 	for {
-		//if global.GitTag == "" || global.GitTagCommitHash == "" {
-		//	fmt.Printf("Example1: I am running! BuildDate: '%s' Commit: '%s' Version: '%s' Now: '%s'\n", global.BuildTime.Format(time.DateTime), global.GitCommitHash, global.Version, time.Now().Format(time.DateTime))
-		//} else {
-		fmt.Printf("Example1: I am running! BuildDate: '%s' Commit: '%s' Tag: '%s' Tag Commit: '%s' Version: '%s' Now: '%s'\n", global.BuildTime.Format(time.DateTime), global.GitCommitHash, global.GitTag, global.GitTagCommitHash, global.Version, time.Now().Format(time.DateTime))
-		//}
+		if global.GitTag == "" || global.GitTagCommitHash == "" {
+			fmt.Printf("Example1: I am running! BuildDate: '%s' Commit: '%s' Version: '%s' Now: '%s'\n", global.BuildTime.Format(time.DateTime), global.GitCommitHash, global.Version, time.Now().Format(time.DateTime))
+		} else {
+			fmt.Printf("Example1: I am running! BuildDate: '%s' Commit: '%s' Tag: '%s' Tag Commit: '%s' Version: '%s' Now: '%s'\n", global.BuildTime.Format(time.DateTime), global.GitCommitHash, global.GitTag, global.GitTagCommitHash, global.Version, time.Now().Format(time.DateTime))
+		}
 
 		select {
 		case <-s.ctx.Listen():

+ 2 - 1
src/server/example2/server.go

@@ -6,6 +6,7 @@ package example2
 
 import (
 	"fmt"
+	"github.com/SongZihuan/BackendServerTemplate/src/config"
 	"github.com/SongZihuan/BackendServerTemplate/src/logger"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/servercontext"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/serverinterface"
@@ -78,7 +79,7 @@ func (s *ServerExample2) Run() {
 
 MainCycle:
 	for {
-		fmt.Println("Example2: I am running!")
+		fmt.Printf("Example2: Hello, %s. I am running!\n", config.Data().Server.Name)
 
 		select {
 		case <-s.ctx.Listen():

+ 12 - 0
src/utils/consoleutils/posix.go

@@ -6,6 +6,11 @@
 
 package consoleutils
 
+import (
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/fileutils"
+	"os"
+)
+
 func FreeConsole() error {
 	return nil
 }
@@ -27,6 +32,9 @@ func MakeNewConsole() error {
 }
 
 func GetConsoleWindow() uintptr {
+	if fileutils.IsFileOpen(os.Stdout) || fileutils.IsFileOpen(os.Stdout) || fileutils.IsFileOpen(os.Stdout) {
+		return 1 // 设置为 1 表示具有 console
+	}
 	return 0
 }
 
@@ -49,3 +57,7 @@ func SetConsoleCP(codePage uint) error {
 func SetConsoleCPSafe(codePage uint) error {
 	return nil
 }
+
+func AttachConsole() error {
+	return nil
+}

+ 74 - 14
src/utils/consoleutils/win32.go

@@ -12,6 +12,8 @@ import (
 	"syscall"
 )
 
+const ATTACH_PARENT_PROCESS = uintptr(^uint32(0)) // 0xFFFFFFFF
+
 var (
 	kernel32 = syscall.NewLazyDLL("kernel32.dll")
 
@@ -22,20 +24,27 @@ var (
 	getConsoleWindow      = kernel32.NewProc("GetConsoleWindow")
 	setConsoleCP          = kernel32.NewProc("SetConsoleCP")
 	setConsoleOutputCP    = kernel32.NewProc("SetConsoleOutputCP")
+	attachConsole         = kernel32.NewProc("AttachConsole")
 )
 
 func FreeConsole() error {
-	ret, _, _ := freeConsole.Call()
+	ret, _, err := freeConsole.Call()
 	if ret == 0 {
-		return fmt.Errorf("FreeConsole error")
+		if err == nil {
+			err = fmt.Errorf("unknown")
+		}
+		return fmt.Errorf("FreeConsole error: %s", err.Error())
 	}
 	return nil
 }
 
 func AllocConsole() error {
-	ret, _, _ := allocConsole.Call()
+	ret, _, err := allocConsole.Call()
 	if ret == 0 {
-		return fmt.Errorf("AllocConsole error")
+		if err == nil {
+			err = fmt.Errorf("unknow")
+		}
+		return fmt.Errorf("AllocConsole error: %s", err.Error())
 	}
 	return nil
 }
@@ -65,7 +74,7 @@ func SetConsoleCtrlHandler(handler func(event uint) bool, add bool) error {
 		_add = 1
 	}
 
-	ret, _, _ := setConsoleCtrlHandler.Call(
+	ret, _, err := setConsoleCtrlHandler.Call(
 		syscall.NewCallback(func(event uint) uintptr {
 			if handler(event) {
 				return 1
@@ -75,19 +84,24 @@ func SetConsoleCtrlHandler(handler func(event uint) bool, add bool) error {
 		_add,
 	)
 	if ret == 0 {
-		return fmt.Errorf("SetConsoleCtrlHandler error")
+		if err == nil {
+			err = fmt.Errorf("unknown")
+		}
+		return fmt.Errorf("SetConsoleCtrlHandler error: %s", err)
 	}
 
 	return nil
 }
 
 func MakeNewConsole(codePage uint) error {
-	err := FreeConsole()
-	if err != nil {
-		return err
+	if HasConsoleWindow() {
+		err := FreeConsole()
+		if err != nil {
+			return err
+		}
 	}
 
-	err = AllocConsole()
+	err := AllocConsole()
 	if err != nil {
 		return err
 	}
@@ -115,17 +129,23 @@ func HasConsoleWindow() bool {
 }
 
 func SetConsoleInputCP(codePage uint) error {
-	ret, _, _ := setConsoleCP.Call(uintptr(codePage))
+	ret, _, err := setConsoleCP.Call(uintptr(codePage))
 	if ret == 0 {
-		return fmt.Errorf("SetConsoleInputCP error")
+		if err == nil {
+			err = fmt.Errorf("unknown")
+		}
+		return fmt.Errorf("SetConsoleInputCP error: %s", err.Error())
 	}
 	return nil
 }
 
 func SetConsoleOutputCP(codePage uint) error {
-	ret, _, _ := setConsoleOutputCP.Call(uintptr(codePage))
+	ret, _, err := setConsoleOutputCP.Call(uintptr(codePage))
 	if ret == 0 {
-		return fmt.Errorf("SetConsoleOutputCP error")
+		if err == nil {
+			err = fmt.Errorf("unknown")
+		}
+		return fmt.Errorf("SetConsoleOutputCP error: %s", err.Error())
 	}
 	return nil
 }
@@ -151,3 +171,43 @@ func SetConsoleCPSafe(codePage uint) error {
 
 	return SetConsoleCP(codePage)
 }
+
+func AttachConsole(ppid int) error {
+	if HasConsoleWindow() {
+		err := FreeConsole()
+		if err != nil {
+			return err
+		}
+	}
+
+	// 定义目标进程 ID
+	ret, _, err := attachConsole.Call(uintptr(ppid))
+	if ret == 0 {
+		if err == nil {
+			err = fmt.Errorf("unknown")
+		}
+		return fmt.Errorf("AttachParentConsole error: %s", err.Error())
+	}
+
+	return nil
+}
+
+func AttachParentConsole() error {
+	if HasConsoleWindow() {
+		err := FreeConsole()
+		if err != nil {
+			return err
+		}
+	}
+
+	// 定义目标进程 ID
+	ret, _, err := attachConsole.Call(ATTACH_PARENT_PROCESS)
+	if ret == 0 {
+		if err == nil {
+			err = fmt.Errorf("unknown")
+		}
+		return fmt.Errorf("AttachParentConsole error: %s", err.Error())
+	}
+
+	return nil
+}

+ 39 - 0
src/utils/envutils/env.go

@@ -0,0 +1,39 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package envutils
+
+import (
+	"strings"
+)
+
+func StringToEnvName(input string) string {
+	// Replace '.' and '-' with '_'
+	replaced := strings.NewReplacer(".", "_", "-", "_").Replace(input)
+
+	// Convert to uppercase
+	upper := strings.ToUpper(replaced)
+
+	// Remove all other symbols
+	cleaned := strings.Map(func(r rune) rune {
+		if r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' {
+			return r
+		}
+		return -1
+	}, upper)
+
+	return cleaned
+}
+
+func GetEnvReplaced() *strings.Replacer {
+	rules := make([]string, 0, (26+2)*2)
+	rules = append(rules, ".", "_", "-", "_")
+
+	for _, i := range "abcdefghijklmnopqrstuvwxyz" {
+		u := strings.ToUpper(string(i))
+		rules = append(rules, string(i), u)
+	}
+
+	return strings.NewReplacer(rules...)
+}

+ 14 - 4
src/utils/exitutils/exit.go

@@ -52,7 +52,7 @@ func InitFailedErrorForWin32ConsoleModule(reason string, exitCode ...int) ExitCo
 
 	ec := getExitCode(exitCodeDefaultError, exitCode...)
 
-	log.Printf("The module `Win32 Console` init failed (reason: `%s`) .", reason)
+	log.Printf("The module `Win32 Console XXX` init failed (reason: `%s`) .", reason)
 	log.Printf("Now we should exit with code %d.", ec)
 
 	return ec
@@ -130,7 +130,7 @@ func SuccessExit(reason string, exitCode ...int) ExitCode {
 
 func SuccessExitSimple(reason string, exitCode ...int) ExitCode {
 	if reason != "" {
-		fmt.Println(reason)
+		log.Println(reason)
 	}
 	return getExitCode(exitCodeDefaultSuccess, exitCode...)
 }
@@ -153,14 +153,24 @@ func Exit(err error) {
 		ExitByCode(ec)
 	} else {
 		if logger.IsReady() {
-			logger.Warnf("Now we should exit with code %d (reason: %s) .", ec, err.Error())
+			logger.Warnf("Now we should exit with code %d (reason: %s) .", exitCodeDefaultError, err.Error())
 		} else {
-			log.Printf("Now we should exit with code %d (reason: %s) .", ec, err.Error())
+			log.Printf("Now we should exit with code %d (reason: %s) .", exitCodeDefaultError, err.Error())
 		}
 		os.Exit(exitCodeDefaultError)
 	}
 }
 
+func ExitQuite(err error) {
+	var ec ExitCode
+	if err == nil {
+		os.Exit(exitCodeDefaultSuccess)
+	} else if errors.As(err, &ec) {
+		ExitByCode(ec)
+	}
+	os.Exit(exitCodeDefaultError)
+}
+
 func ExitByCode(ec ExitCode) {
 	os.Exit(int(ec))
 }

+ 25 - 0
src/utils/fileutils/file.go

@@ -0,0 +1,25 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package fileutils
+
+import "os"
+
+func IsFileOpen(file *os.File) bool {
+	// 获取文件描述符
+	fd := file.Fd()
+
+	// 检查文件描述符是否有效
+	if fd < 0 {
+		return false
+	}
+
+	// 尝试对文件描述符进行简单的读/写操作以确认其状态
+	fileInfo, err := file.Stat()
+	if err != nil || fileInfo == nil {
+		return false
+	}
+
+	return true
+}

+ 9 - 5
src/utils/osutils/export.go

@@ -12,7 +12,7 @@ import (
 
 var _args0 = ""
 var _args0Name = ""
-var _args0NamePosix = ""
+var _args0NamePOSIX = ""
 
 func init() {
 	var err error
@@ -35,10 +35,10 @@ func init() {
 		panic("_args0Name was empty")
 	}
 
-	_args0NamePosix = strings.TrimSuffix(_args0Name, ".exe")
+	_args0NamePOSIX = strings.TrimSuffix(_args0Name, ".exe")
 
-	if _args0NamePosix == "" {
-		panic("_args0NamePosix was empty")
+	if _args0NamePOSIX == "" {
+		panic("_args0NamePOSIX was empty")
 	}
 }
 
@@ -47,5 +47,9 @@ func GetArgs0() string {
 }
 
 func GetArgs0Name() string {
-	return filepath.Base(_args0Name)
+	return _args0Name
+}
+
+func GetArgs0NamePOSIX() string {
+	return _args0NamePOSIX
 }

+ 9 - 0
src/utils/sliceutils/slice.go

@@ -9,3 +9,12 @@ func CopySlice[T any](src []T) []T {
 	copy(dest, src)
 	return dest
 }
+
+func SliceHasItem[T comparable](src []T, item T) bool {
+	for _, i := range src {
+		if i == item {
+			return true
+		}
+	}
+	return false
+}