소스 검색

添加 utils 包的单元测试函数

添加单元测试函数。适当重构部分包的结构。重命名部分函数和属性。删除部分未使用的函数。修改部分函数逻辑。
SongZihuan 15 시간 전
부모
커밋
3b46dd65b0
43개의 변경된 파일1049개의 추가작업 그리고 357개의 파일을 삭제
  1. 6 0
      CHANGELOG.md
  2. 2 2
      src/cmd/catv1/main.go
  3. 2 2
      src/cmd/lionv1/main.go
  4. 11 6
      src/cmd/prerun/main.go
  5. 2 2
      src/cmd/tigerv1/main.go
  6. 1 1
      src/config/configparser/json.go
  7. 1 1
      src/config/configparser/yaml.go
  8. 1 1
      src/config/global_config.go
  9. 16 1
      src/global/variable.go
  10. 2 5
      src/logger/write/datefilewriter/writer.go
  11. 2 5
      src/logger/write/filewriter/writer.go
  12. 51 0
      src/utils/cleanstringutils/oneline_test.go
  13. 2 0
      src/utils/consoleutils/global.go
  14. 0 8
      src/utils/consoleutils/posix.go
  15. 11 37
      src/utils/envutils/env.go
  16. 67 0
      src/utils/envutils/env_test.go
  17. 38 0
      src/utils/exitutils/error_exit.go
  18. 0 200
      src/utils/exitutils/exit.go
  19. 64 0
      src/utils/exitutils/exitcode.go
  20. 71 0
      src/utils/exitutils/exitcode_test.go
  21. 37 0
      src/utils/exitutils/init_exit.go
  22. 34 0
      src/utils/exitutils/run_exit.go
  23. 30 0
      src/utils/exitutils/success_exit.go
  24. 106 0
      src/utils/filesystemutils/exists_test.go
  25. 4 0
      src/utils/fileutils/file.go
  26. 49 0
      src/utils/fileutils/file_test.go
  27. 25 13
      src/utils/formatutils/format.go
  28. 30 0
      src/utils/formatutils/format_test.go
  29. 8 0
      src/utils/formatutils/test_file_1.txt
  30. 21 0
      src/utils/formatutils/test_file_2.txt
  31. 0 16
      src/utils/reflectutils/reflect.go
  32. 61 0
      src/utils/reutils/regex_test.go
  33. 33 0
      src/utils/sliceutils/slice_test.go
  34. 16 0
      src/utils/strconvutils/number.go
  35. 21 0
      src/utils/strconvutils/number_test.go
  36. 36 41
      src/utils/strconvutils/time.go
  37. 42 0
      src/utils/strconvutils/time_test.go
  38. 1 1
      src/utils/timeutils/local_timezone_posix.go
  39. 1 1
      src/utils/timeutils/local_timezone_win32.go
  40. 50 0
      src/utils/timeutils/timezone_test.go
  41. 0 14
      src/utils/typeutils/stringbool.go
  42. 88 0
      src/utils/typeutils/stringbool_test.go
  43. 6 0
      third-party/github.com.SongZihuan.BackendServerTemplate/CHANGELOG.md

+ 6 - 0
CHANGELOG.md

@@ -4,6 +4,12 @@
 
 其格式基于 [CHANGELOG 准则](./CHANGELOG_SPECIFICATION.md) 。
 
+## [未发布]
+
+### 新增
+
+- 添加`utils`包的单元测试。
+
 ## [0.13.0] - 2025/04/28 Asia/Shanghai
 
 ### 新增

+ 2 - 2
src/cmd/catv1/main.go

@@ -35,7 +35,7 @@ var name string = global.Name
 var inputConfigFilePath string = "config.yaml"
 
 func main() {
-	command().Init().Exit()
+	command().ClampAttribute().Exit()
 }
 
 func command() exitutils.ExitCode {
@@ -196,5 +196,5 @@ func command() exitutils.ExitCode {
 	}
 
 	cmd.AddCommand(version.CMD, license.CMD, report.CMD, check.CMD, install, uninstall, start, stop, restart)
-	return exitutils.ExitQuite(cmd.Execute())
+	return exitutils.ErrorToExitQuite(cmd.Execute())
 }

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

@@ -31,7 +31,7 @@ var reload bool = false
 var ppid int = 0
 
 func main() {
-	command().Init().Exit()
+	command().ClampAttribute().Exit()
 }
 
 func command() exitutils.ExitCode {
@@ -104,5 +104,5 @@ func command() exitutils.ExitCode {
 
 	cmd.Flags().StringVarP(&inputConfigFilePath, "config", "c", inputConfigFilePath, "the file path of the configuration file")
 
-	return exitutils.ExitQuite(cmd.Execute())
+	return exitutils.ErrorToExitQuite(cmd.Execute())
 }

+ 11 - 6
src/cmd/prerun/main.go

@@ -21,31 +21,36 @@ import (
 func PreRun() (exitCode error) {
 	var err error
 
-	quiteMode := envutils.GetEnv("QUITE")
+	quiteMode := envutils.GetEnv(global.EnvPrefix, "QUITE")
 	if quiteMode != "" {
 		err = stdutils.QuiteMode()
 		if err != nil {
 			stdutils.Recover()
-			return exitutils.InitFailedForQuiteModeModule(err.Error())
+			return exitutils.InitFailed("Quite Mode", err.Error())
 		}
 	}
 
 	if global.UTCLocation == nil {
-		return exitutils.InitFailedForTimeLocationModule("can not get utc location")
+		return exitutils.InitFailed("Time Location", "can not get utc location")
 	}
 
 	if global.LocalLocation == nil {
-		return exitutils.InitFailedForTimeLocationModule("can not get local location")
+		return exitutils.InitFailed("Time Location", "can not get local location")
 	}
 
 	err = consoleutils.SetConsoleCPSafe(consoleutils.CodePageUTF8)
 	if err != nil {
-		return exitutils.InitFailedForWin32ConsoleModule(err.Error())
+		return exitutils.InitFailed("Win32 Console API", err.Error())
 	}
 
 	err = logger.InitBaseLogger()
 	if err != nil {
-		return exitutils.InitFailedForLoggerModule(err.Error())
+		return exitutils.InitFailed("Logger", err.Error())
+	}
+
+	err = stdutils.OpenNullFile()
+	if err != nil {
+		return exitutils.InitFailed("File dev/null", err.Error())
 	}
 
 	return nil

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

@@ -31,7 +31,7 @@ var reload bool = false
 var ppid int = 0
 
 func main() {
-	command().Init().Exit()
+	command().ClampAttribute().Exit()
 }
 
 func command() exitutils.ExitCode {
@@ -104,5 +104,5 @@ func command() exitutils.ExitCode {
 
 	cmd.Flags().StringVarP(&inputConfigFilePath, "config", "c", inputConfigFilePath, "the file path of the configuration file")
 
-	return exitutils.ExitQuite(cmd.Execute())
+	return exitutils.ErrorToExitQuite(cmd.Execute())
 }

+ 1 - 1
src/config/configparser/json.go

@@ -36,7 +36,7 @@ func NewJsonProvider(opt *NewConfigParserProviderOption) *JsonProvider {
 
 	// 环境变量
 	p.viper.SetEnvPrefix(opt.EnvPrefix)
-	p.viper.SetEnvKeyReplacer(envutils.GetEnvReplaced())
+	p.viper.SetEnvKeyReplacer(envutils.EnvReplacer)
 	p.viper.AutomaticEnv()
 
 	if p.autoReload {

+ 1 - 1
src/config/configparser/yaml.go

@@ -36,7 +36,7 @@ func NewYamlProvider(opt *NewConfigParserProviderOption) *YamlProvider {
 
 	// 环境变量
 	p.viper.SetEnvPrefix(opt.EnvPrefix)
-	p.viper.SetEnvKeyReplacer(envutils.GetEnvReplaced())
+	p.viper.SetEnvKeyReplacer(envutils.EnvReplacer)
 	p.viper.AutomaticEnv()
 
 	if p.autoReload {

+ 1 - 1
src/config/global_config.go

@@ -84,7 +84,7 @@ func (d *GlobalConfig) process(c *configInfo) (cfgErr configerror.Error) {
 		}
 	} else {
 		var err error
-		location, err = timeutils.LoadLocation(d.Timezone)
+		location, err = timeutils.LoadTimezone(d.Timezone)
 		if err != nil || location == nil {
 			location = global.UTCLocation
 		}

+ 16 - 1
src/global/variabl.go → src/global/variable.go

@@ -5,9 +5,11 @@
 package global
 
 import (
+	"fmt"
 	resource "github.com/SongZihuan/BackendServerTemplate"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/envutils"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/timeutils"
+	"strings"
 	"time"
 )
 
@@ -21,7 +23,7 @@ var (
 	GitCommitHash      = resource.GitCommitHash
 	GitTag             = resource.GitTag
 	GitTagCommitHash   = resource.GitTagCommitHash
-	EnvPrefix          = envutils.EnvPrefix
+	EnvPrefix          = resource.EnvPrefix
 
 	// Name 继承自resource
 	// 注意:命令行参数或配置文件加载时可能会被更改
@@ -35,3 +37,16 @@ var (
 	LocalLocation = timeutils.GetLocalTimezone()
 	Location      = time.UTC
 )
+
+func init() {
+	if EnvPrefix == "" {
+		return
+	}
+
+	newEnvPrefix := envutils.ToEnvName(EnvPrefix)
+	if EnvPrefix != newEnvPrefix {
+		panic(fmt.Errorf("bad %s; good %s", EnvPrefix, newEnvPrefix))
+	} else if strings.HasSuffix(EnvPrefix, "_") {
+		panic("EnvPrefix End With '_'")
+	}
+}

+ 2 - 5
src/logger/write/datefilewriter/writer.go

@@ -9,6 +9,7 @@ import (
 	"github.com/SongZihuan/BackendServerTemplate/src/logger/logformat"
 	"github.com/SongZihuan/BackendServerTemplate/src/logger/write"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/filesystemutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/fileutils"
 	"os"
 	"path"
 	"time"
@@ -37,11 +38,7 @@ func (f *DateFileWriter) Write(data *logformat.LogData) (n int, err error) {
 		}
 	}
 
-	if f.file == nil {
-		return 0, fmt.Errorf("file writer has been close")
-	}
-
-	if f.file.Fd() == ^(uintptr(0)) { // 检查文件描述符是否为 -1
+	if fileutils.IsFileOpen(f.file) {
 		return 0, fmt.Errorf("file writer has been close")
 	}
 

+ 2 - 5
src/logger/write/filewriter/writer.go

@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"github.com/SongZihuan/BackendServerTemplate/src/logger/logformat"
 	"github.com/SongZihuan/BackendServerTemplate/src/logger/write"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/fileutils"
 	"os"
 )
 
@@ -18,11 +19,7 @@ type FileWriter struct {
 }
 
 func (f *FileWriter) Write(data *logformat.LogData) (n int, err error) {
-	if f.file == nil {
-		return 0, fmt.Errorf("file writer has been close")
-	}
-
-	if f.file.Fd() == ^(uintptr(0)) { // 检查文件描述符是否为 -1
+	if fileutils.IsFileOpen(f.file) {
 		return 0, fmt.Errorf("file writer has been close")
 	}
 

+ 51 - 0
src/utils/cleanstringutils/oneline_test.go

@@ -0,0 +1,51 @@
+// 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 cleanstringutils
+
+import "testing"
+
+func TestGetStringOneLine(t *testing.T) {
+	t.Run("Text-OnlyLine", func(t *testing.T) {
+		text := "Hello"
+		if GetStringOneLine(text) != "Hello" {
+			t.Errorf("ClenFileData OnlyLine error")
+		}
+	})
+
+	t.Run("Text-OnlyLine-WithSpace", func(t *testing.T) {
+		text := "Hello    "
+		if GetStringOneLine(text) != "Hello" {
+			t.Errorf("ClenFileData OnlyLine error")
+		}
+	})
+
+	t.Run("Text-With-CRLF", func(t *testing.T) {
+		text := "Hello\r\n"
+		if GetStringOneLine(text) != "Hello" {
+			t.Errorf("ClenFileData OnlyLine error")
+		}
+	})
+
+	t.Run("Text-With-More-CRLF", func(t *testing.T) {
+		text := "Hello\r\n\r\n\r\n"
+		if GetStringOneLine(text) != "Hello" {
+			t.Errorf("ClenFileData OnlyLine error")
+		}
+	})
+
+	t.Run("Text-With-CRLF-WithSpace", func(t *testing.T) {
+		text := "Hello    \r\n"
+		if GetStringOneLine(text) != "Hello" {
+			t.Errorf("ClenFileData OnlyLine error")
+		}
+	})
+
+	t.Run("Text-MoreLine", func(t *testing.T) {
+		text := "Hello\r\nWorld"
+		if GetStringOneLine(text) != "Hello" {
+			t.Errorf("ClenFileData OnlyLine error")
+		}
+	})
+}

+ 2 - 0
src/utils/consoleutils/global.go

@@ -31,6 +31,8 @@ func (e *EventData) GetCode() uint {
 func (*EventData) ConsoleEvent() {}
 
 // 定义控制台事件类型
+//
+//goland:noinspection GoSnakeCaseUsage
 var (
 	CTRL_C_EVENT Event = &EventData{
 		Name: "CTRL_C_EVENT",

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

@@ -6,11 +6,6 @@
 
 package consoleutils
 
-import (
-	"github.com/SongZihuan/BackendServerTemplate/src/utils/fileutils"
-	"os"
-)
-
 func FreeConsole() error {
 	return nil
 }
@@ -32,9 +27,6 @@ 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
 }
 

+ 11 - 37
src/utils/envutils/env.go

@@ -6,45 +6,13 @@ package envutils
 
 import (
 	"fmt"
-	resource "github.com/SongZihuan/BackendServerTemplate"
 	"os"
 	"strings"
 )
 
-var EnvPrefix = resource.EnvPrefix
+var EnvReplacer *strings.Replacer = nil
 
 func init() {
-	if EnvPrefix == "" {
-		return
-	}
-
-	newEnvPrefix := StringToEnvName(EnvPrefix)
-	if EnvPrefix != newEnvPrefix {
-		panic(fmt.Errorf("bad %s; good %s", EnvPrefix, newEnvPrefix))
-	} else if strings.HasSuffix(EnvPrefix, "_") {
-		panic("EnvPrefix End With '_'")
-	}
-}
-
-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, ".", "_", "-", "_", " ", "")
 
@@ -53,16 +21,22 @@ func GetEnvReplaced() *strings.Replacer {
 		rules = append(rules, string(i), u)
 	}
 
-	return strings.NewReplacer(rules...)
+	EnvReplacer = strings.NewReplacer(rules...)
+}
+
+func ToEnvName(input string) string {
+	return EnvReplacer.Replace(input)
 }
 
 func GetSysEnv(name string) string {
 	return os.Getenv(name)
 }
 
-func GetEnv(name string) string {
-	if resource.EnvPrefix != "" {
-		return os.Getenv(fmt.Sprintf("%s_%s", resource.EnvPrefix, name))
+func GetEnv(prefix string, name string) string {
+	name = ToEnvName(name)
+
+	if prefix != "" {
+		return os.Getenv(fmt.Sprintf("%s_%s", prefix, name))
 	}
 	return os.Getenv(name)
 }

+ 67 - 0
src/utils/envutils/env_test.go

@@ -0,0 +1,67 @@
+// 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 (
+	"os"
+	"testing"
+)
+
+func TestToEnvName(t *testing.T) {
+	if res := ToEnvName("AC"); res != "AC" {
+		t.Errorf("to env name error: AC -> AC: %s", res)
+	}
+
+	if res := ToEnvName("abc"); res != "ABC" {
+		t.Errorf("to env name error: abc -> ABC: %s", res)
+	}
+
+	if res := ToEnvName("A.C"); res != "A_C" {
+		t.Errorf("to env name error: A.C -> A_C: %s", res)
+	}
+
+	if res := ToEnvName("a.c"); res != "A_C" {
+		t.Errorf("to env name error: a.c -> A_C: %s", res)
+	}
+}
+
+func TestGetSysEnv(t *testing.T) {
+	var err error
+
+	err = os.Setenv("TEST_A", "1")
+	if err != nil {
+		t.Fatalf("set env TEST_A error: %s", err.Error())
+	}
+
+	err = os.Setenv("test_b", "2")
+	if err != nil {
+		t.Fatalf("set env TEST_A error: %s", err.Error())
+	}
+
+	if res := GetSysEnv("TEST_A"); res != "1" {
+		t.Errorf("get sys env error: TEST_A -> 1: %s", res)
+	}
+
+	if res := GetSysEnv("test_b"); res != "2" {
+		t.Errorf("get sys env error: test_a -> 2: %s", res)
+	}
+}
+
+func TestGetEnv(t *testing.T) {
+	var err error
+
+	err = os.Setenv("P_R_TEST_C", "3")
+	if err != nil {
+		t.Fatalf("set env TEST_A error: %s", err.Error())
+	}
+
+	if res := GetEnv("P_R", "TEST_C"); res != "3" {
+		t.Errorf("get env error: P_R TEST_C -> 1: %s", res)
+	}
+
+	if res := GetEnv("P_R", "test.c"); res != "3" {
+		t.Errorf("get env error: P_R TEST_C -> 1: %s", res)
+	}
+}

+ 38 - 0
src/utils/exitutils/error_exit.go

@@ -0,0 +1,38 @@
+// 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 exitutils
+
+import (
+	"errors"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"log"
+)
+
+func ErrorToExit(err error) ExitCode {
+	var ec ExitCode
+	if err == nil {
+		return exitCodeDefaultSuccess
+	} else if errors.As(err, &ec) {
+		return ec
+	} else {
+		if logger.IsReady() {
+			logger.Errorf("Exit %d: %s", ec, err.Error())
+		} else {
+			log.Printf("Exit %d: %s\n", ec, err.Error())
+		}
+		return exitCodeDefaultError
+	}
+}
+
+func ErrorToExitQuite(err error) ExitCode {
+	var ec ExitCode
+	if err == nil {
+		return exitCodeDefaultSuccess
+	} else if errors.As(err, &ec) {
+		return ec
+	} else {
+		return exitCodeDefaultError
+	}
+}

+ 0 - 200
src/utils/exitutils/exit.go

@@ -1,200 +0,0 @@
-// 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 exitutils
-
-import (
-	"errors"
-	"fmt"
-	"github.com/SongZihuan/BackendServerTemplate/src/logger"
-	"log"
-	"os"
-)
-
-const (
-	exitCodeMin                 = 0
-	exitCodeMax                 = 255
-	exitCodeDefaultSuccess      = 0   // 默认值:正常
-	exitCodeDefaultError        = 1   // 默认值:错误
-	exitCodeInitFailedError     = 2   // 初始化错误
-	exitCodeRunError            = 3   // 运行时错误
-	exitCodeRunErrorQuite       = 4   // 运行时错误(安静关闭)
-	exitCodeReload              = 252 // 重启信号
-	exitCodeWithUnknownError    = 253 // 未知错误
-	exitCodeErrorLogMustBeReady = 254 // 报告该错误需要日志系统加载完成
-)
-
-const ExitCodeReload = exitCodeReload
-
-type ExitCode int
-
-func (e ExitCode) Error() string {
-	return fmt.Sprintf("Exit with code %d", e)
-}
-
-func (e ExitCode) Init() ExitCode {
-	if e < exitCodeMin {
-		e = -e
-	}
-
-	if e > exitCodeMax {
-		e = exitCodeMax
-	}
-
-	return e
-}
-
-func (e ExitCode) Exit() {
-	os.Exit(int(e))
-}
-
-func (e ExitCode) ChangeToLoggerNotReady() ExitCode {
-	if e == exitCodeDefaultError || e == exitCodeWithUnknownError {
-		return exitCodeErrorLogMustBeReady
-	}
-	return e
-}
-
-func getExitCode(defaultExitCode int, exitCode ...int) (ec ExitCode) {
-	if len(exitCode) == 1 {
-		ec = ExitCode(exitCode[0])
-	} else {
-		ec = ExitCode(defaultExitCode)
-	}
-
-	return ec.Init()
-}
-
-func initModuleFailedLog(module string, reason string) string {
-	if module == "" {
-		panic("module can not be empty")
-	}
-
-	if reason != "" {
-		return fmt.Sprintf("Init failed [ %s ]: %s", module, reason)
-	} else {
-		return fmt.Sprintf("Init failed [ %s ]", module)
-	}
-}
-
-func InitFailedForWin32ConsoleModule(reason string, exitCode ...int) ExitCode {
-	ec := getExitCode(exitCodeInitFailedError, exitCode...)
-
-	log.Printf(initModuleFailedLog("Win32 Console API", reason))
-	log.Printf("Init error exit %d: failed", ec)
-
-	return ec
-}
-
-func InitFailedForQuiteModeModule(reason string, exitCode ...int) ExitCode {
-	ec := getExitCode(exitCodeInitFailedError, exitCode...)
-
-	log.Printf(initModuleFailedLog("Quite Mode", reason))
-	log.Printf("Init error exit %d: failed", ec)
-
-	return ec
-}
-
-func InitFailedForTimeLocationModule(reason string, exitCode ...int) ExitCode {
-	ec := getExitCode(exitCodeInitFailedError, exitCode...)
-
-	log.Printf(initModuleFailedLog("Time Location", reason))
-	log.Printf("Init error exit %d: failed", ec)
-
-	return ec
-}
-
-func InitFailedForLoggerModule(reason string, exitCode ...int) ExitCode {
-	ec := getExitCode(exitCodeInitFailedError, exitCode...)
-
-	log.Printf(initModuleFailedLog("Logger", reason))
-	log.Printf("Init error exit %d: failed", ec)
-
-	return ec
-}
-
-func InitFailed(module string, reason string, exitCode ...int) ExitCode {
-	ec := getExitCode(exitCodeInitFailedError, exitCode...)
-
-	if logger.IsReady() {
-		logger.Error(initModuleFailedLog(module, reason))
-		logger.Errorf("Init error exit %d: failed", ec)
-		return ec
-	} else {
-		log.Println(initModuleFailedLog(module, reason))
-		log.Printf("Init error exit %d: failed\n", ec)
-		return ec.ChangeToLoggerNotReady()
-	}
-}
-
-func RunErrorQuite(exitCode ...int) ExitCode {
-	return getExitCode(exitCodeRunErrorQuite, exitCode...)
-}
-
-func RunError(reason string, exitCode ...int) ExitCode {
-	ec := getExitCode(exitCodeRunError, exitCode...)
-
-	if logger.IsReady() {
-		if reason != "" {
-			logger.Errorf("Run error exit %d: %s", ec, reason)
-		} else {
-			logger.Errorf("Run error exit %d: failed", ec)
-		}
-		return ec
-	} else {
-		if reason != "" {
-			log.Printf("Run error exit %d: %s\n", ec, reason)
-		} else {
-			log.Printf("Run error exit %d: failed\n", ec)
-		}
-		return ec.ChangeToLoggerNotReady()
-	}
-}
-
-func SuccessExit(reason string, exitCode ...int) ExitCode {
-	ec := getExitCode(exitCodeDefaultSuccess, exitCode...)
-
-	if logger.IsReady() {
-		if reason != "" {
-			logger.Warnf("Exit %d: %s", ec, reason)
-		} else {
-			logger.Warnf("Exit %d: ok", ec)
-		}
-	} else {
-		if reason != "" {
-			log.Printf("Exit %d: %s\n", ec, reason)
-		} else {
-			log.Printf("Exit %d: ok\n", ec)
-		}
-	}
-
-	return ec // ec 不再受 logger 的 ready 问题影响
-}
-
-func ErrorToExit(err error) ExitCode {
-	var ec ExitCode
-	if err == nil {
-		return exitCodeDefaultSuccess
-	} else if errors.As(err, &ec) {
-		return ec
-	} else {
-		if logger.IsReady() {
-			logger.Errorf("Exit %d: %s", ec, err.Error())
-		} else {
-			log.Printf("Exit %d: %s\n", ec, err.Error())
-		}
-		return exitCodeDefaultError
-	}
-}
-
-func ExitQuite(err error) ExitCode {
-	var ec ExitCode
-	if err == nil {
-		return exitCodeDefaultSuccess
-	} else if errors.As(err, &ec) {
-		return ec
-	} else {
-		return exitCodeDefaultError
-	}
-}

+ 64 - 0
src/utils/exitutils/exitcode.go

@@ -0,0 +1,64 @@
+// 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 exitutils
+
+import (
+	"fmt"
+	"os"
+)
+
+const (
+	// 最大值和最小值
+	exitCodeMin = 0
+	exitCodeMax = 255
+
+	// 默认值
+	exitCodeDefaultSuccess = 0 // 默认值:正常
+	exitCodeDefaultError   = 1 // 默认值:错误
+
+	// 特殊错误值
+	exitCodeInitFailedError  = 2   // 初始化错误
+	exitCodeRunError         = 3   // 运行时错误
+	exitCodeRunErrorQuite    = 4   // 运行时错误(安静关闭)
+	exitCodeReload           = 252 // 重启信号
+	exitCodeWithUnknownError = 253 // 未知错误
+	// 254: 原定为 `Logger` 错误的退出码,现已取消。
+)
+
+const ExitCodeReload = exitCodeReload
+
+type ExitCode int
+
+func (e ExitCode) Error() string {
+	return fmt.Sprintf("Exit with code %d", e)
+}
+
+func (e ExitCode) ClampAttribute() ExitCode {
+	res := e
+
+	if res < exitCodeMin {
+		res = -res
+	}
+
+	if res > exitCodeMax {
+		res = exitCodeMax
+	}
+
+	return res
+}
+
+func (e ExitCode) Exit() {
+	os.Exit(int(e))
+}
+
+func getExitCode(defaultExitCode int, exitCode ...int) (ec ExitCode) {
+	if len(exitCode) == 1 {
+		ec = ExitCode(exitCode[0])
+	} else {
+		ec = ExitCode(defaultExitCode)
+	}
+
+	return ec.ClampAttribute()
+}

+ 71 - 0
src/utils/exitutils/exitcode_test.go

@@ -0,0 +1,71 @@
+// 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 exitutils
+
+import "testing"
+
+func TestExitCode(t *testing.T) {
+	if res := ExitCode(0).ClampAttribute(); res != 0 {
+		t.Errorf("ClampAttribute Error: ExitCode(0) -> 0: %d", res)
+	}
+
+	if res := ExitCode(1).ClampAttribute(); res != 1 {
+		t.Errorf("ClampAttribute Error: ExitCode(1) -> 1: %d", res)
+	}
+
+	if res := ExitCode(-5).ClampAttribute(); res != 5 {
+		t.Errorf("ClampAttribute Error: ExitCode(-5) -> 5: %d", res)
+	}
+
+	if res := ExitCode(300).ClampAttribute(); res != 255 {
+		t.Errorf("ClampAttribute Error: ExitCode(300) -> 255: %d", res)
+	}
+
+	if res := ExitCode(-400).ClampAttribute(); res != 255 {
+		t.Errorf("ClampAttribute Error: ExitCode(-400) -> 255: %d", res)
+	}
+}
+
+func TestGetExitCode(t *testing.T) {
+	if res := getExitCode(0); res != 0 {
+		t.Errorf("ClampAttribute Error: getExitCode(0) -> 0: %d", res)
+	}
+
+	if res := getExitCode(1); res != 1 {
+		t.Errorf("ClampAttribute Error: getExitCode(1) -> 1: %d", res)
+	}
+
+	if res := getExitCode(1, 0); res != 0 {
+		t.Errorf("ClampAttribute Error: getExitCode(1, 0) -> 0: %d", res)
+	}
+
+	if res := getExitCode(1, 0, 2); res != 1 {
+		t.Errorf("ClampAttribute Error: getExitCode(1, 0, 2) -> 1: %d", res)
+	}
+
+	if res := getExitCode(-1); res != 1 {
+		t.Errorf("ClampAttribute Error: getExitCode(-1) -> 1: %d", res)
+	}
+
+	if res := getExitCode(400); res != 255 {
+		t.Errorf("ClampAttribute Error: getExitCode(400) -> 255: %d", res)
+	}
+
+	if res := getExitCode(-300); res != 255 {
+		t.Errorf("ClampAttribute Error: getExitCode(-300) -> 255: %d", res)
+	}
+
+	if res := getExitCode(0, -1); res != 1 {
+		t.Errorf("ClampAttribute Error: getExitCode(0, -1) -> 1: %d", res)
+	}
+
+	if res := getExitCode(0, 400); res != 255 {
+		t.Errorf("ClampAttribute Error: getExitCode(0, 400) -> 255: %d", res)
+	}
+
+	if res := getExitCode(0, -300); res != 255 {
+		t.Errorf("ClampAttribute Error: getExitCode(0, -300) -> 255: %d", res)
+	}
+}

+ 37 - 0
src/utils/exitutils/init_exit.go

@@ -0,0 +1,37 @@
+// 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 exitutils
+
+import (
+	"fmt"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"log"
+)
+
+func initModuleFailedLog(module string, reason string) string {
+	if module == "" {
+		panic("module can not be empty")
+	}
+
+	if reason != "" {
+		return fmt.Sprintf("Init failed [ %s ]: %s", module, reason)
+	} else {
+		return fmt.Sprintf("Init failed [ %s ]", module)
+	}
+}
+
+func InitFailed(module string, reason string, exitCode ...int) ExitCode {
+	ec := getExitCode(exitCodeInitFailedError, exitCode...)
+
+	if logger.IsReady() {
+		logger.Error(initModuleFailedLog(module, reason))
+		logger.Errorf("Init error exit %d: failed", ec)
+		return ec
+	} else {
+		log.Println(initModuleFailedLog(module, reason))
+		log.Printf("Init error exit %d: failed\n", ec)
+		return ec
+	}
+}

+ 34 - 0
src/utils/exitutils/run_exit.go

@@ -0,0 +1,34 @@
+// 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 exitutils
+
+import (
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"log"
+)
+
+func RunErrorQuite(exitCode ...int) ExitCode {
+	return getExitCode(exitCodeRunErrorQuite, exitCode...)
+}
+
+func RunError(reason string, exitCode ...int) ExitCode {
+	ec := getExitCode(exitCodeRunError, exitCode...)
+
+	if logger.IsReady() {
+		if reason != "" {
+			logger.Errorf("Run error exit %d: %s", ec, reason)
+		} else {
+			logger.Errorf("Run error exit %d: failed", ec)
+		}
+		return ec
+	} else {
+		if reason != "" {
+			log.Printf("Run error exit %d: %s\n", ec, reason)
+		} else {
+			log.Printf("Run error exit %d: failed\n", ec)
+		}
+		return ec
+	}
+}

+ 30 - 0
src/utils/exitutils/success_exit.go

@@ -0,0 +1,30 @@
+// 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 exitutils
+
+import (
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"log"
+)
+
+func SuccessExit(reason string, exitCode ...int) ExitCode {
+	ec := getExitCode(exitCodeDefaultSuccess, exitCode...)
+
+	if logger.IsReady() {
+		if reason != "" {
+			logger.Warnf("Exit %d: %s", ec, reason)
+		} else {
+			logger.Warnf("Exit %d: ok", ec)
+		}
+	} else {
+		if reason != "" {
+			log.Printf("Exit %d: %s\n", ec, reason)
+		} else {
+			log.Printf("Exit %d: ok\n", ec)
+		}
+	}
+
+	return ec // ec 不再受 logger 的 ready 问题影响
+}

+ 106 - 0
src/utils/filesystemutils/exists_test.go

@@ -0,0 +1,106 @@
+// 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 filesystemutils
+
+import (
+	"os"
+	"path"
+	"testing"
+)
+
+func TestIsExists(t *testing.T) {
+	temp, err := os.MkdirTemp("", "test*")
+	if err != nil {
+		t.Fatalf("create temp directory failed: %s", temp)
+	}
+	defer func() {
+		_ = os.RemoveAll(temp)
+	}()
+
+	testDir := path.Join(temp, "test_dir")
+	err = os.Mkdir(testDir, 0700)
+	if err != nil {
+		t.Fatalf("create temp/test_dir failed: %s", temp)
+	}
+
+	testFile := path.Join(temp, "test_file")
+	f, err := os.OpenFile(testFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		t.Fatalf("create temp/test_file failed: %s", temp)
+	}
+	_ = f.Close()
+
+	testNotExists := path.Join(temp, "test_not_exists")
+
+	// IsExists 测试
+
+	if !IsExists(testFile) {
+		t.Errorf("IsExists(%s) -> true: false", testFile)
+	}
+
+	if !IsExists(testDir) {
+		t.Errorf("IsExists(%s) -> true: false", testDir)
+	}
+
+	if IsExists(testNotExists) {
+		t.Errorf("IsExists(%s) -> false: true", testNotExists)
+	}
+
+	// IsFile 测试
+
+	if !IsFile(testFile) {
+		t.Errorf("IsFile(%s) -> true: false", testFile)
+	}
+
+	if IsFile(testDir) {
+		t.Errorf("IsFile(%s) -> false: true", testDir)
+	}
+
+	if IsFile(testNotExists) {
+		t.Errorf("IsFile(%s) -> false: true", testNotExists)
+	}
+
+	// IsExistsAndFile 测试
+
+	if exists, file := IsExistsAndFile(testFile); !(exists && file) {
+		t.Errorf("IsExistsAndFile(%s) -> true, true: %v, %v", testDir, exists, file)
+	}
+
+	if exists, file := IsExistsAndFile(testDir); !(exists && !file) {
+		t.Errorf("IsExistsAndFile(%s) -> true, false: %v, %v", testDir, exists, file)
+	}
+
+	if exists, file := IsExistsAndFile(testNotExists); !(!exists && !file) {
+		t.Errorf("IsExistsAndFile(%s) -> false, false: %v, %v", testDir, exists, file)
+	}
+
+	// IsDir 测试
+
+	if IsDir(testFile) {
+		t.Errorf("IsDir(%s) -> false: true", testFile)
+	}
+
+	if !IsDir(testDir) {
+		t.Errorf("IsDir(%s) -> true: false", testDir)
+	}
+
+	if IsDir(testNotExists) {
+		t.Errorf("IsDir(%s) -> false: true", testNotExists)
+	}
+
+	// IsExistsAndDir 测试
+
+	if exists, dir := IsExistsAndDir(testFile); !(exists && !dir) {
+		t.Errorf("IsExistsAndDir(%s) -> true, false: %v, %v", testDir, exists, dir)
+	}
+
+	if exists, dir := IsExistsAndDir(testDir); !(exists && dir) {
+		t.Errorf("IsExistsAndDir(%s) -> true, true: %v, %v", testDir, exists, dir)
+	}
+
+	if exists, dir := IsExistsAndDir(testNotExists); !(!exists && !dir) {
+		t.Errorf("IsExistsAndDir(%s) -> false, false: %v, %v", testDir, exists, dir)
+	}
+}

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

@@ -7,6 +7,10 @@ package fileutils
 import "os"
 
 func IsFileOpen(file *os.File) bool {
+	if file == nil {
+		return false
+	}
+
 	// 获取文件描述符
 	fd := file.Fd()
 

+ 49 - 0
src/utils/fileutils/file_test.go

@@ -0,0 +1,49 @@
+// 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"
+	"path"
+	"testing"
+)
+
+func TestFileIsOpen(t *testing.T) {
+	temp, err := os.MkdirTemp("", "test*")
+	if err != nil {
+		t.Fatalf("create temp directory failed: %s", temp)
+	}
+	defer func() {
+		_ = os.RemoveAll(temp)
+	}()
+
+	fileClose, err := os.OpenFile(path.Join(temp, "test_file_close"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		t.Fatalf("create temp/test_file failed: %s", temp)
+	}
+	_ = fileClose.Close()
+
+	fileOpen, err := os.OpenFile(path.Join(temp, "test_file_open"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		t.Fatalf("create temp/test_file failed: %s", temp)
+	}
+	defer func() {
+		_ = fileOpen.Close()
+	}()
+
+	fileNil := (*os.File)(nil)
+
+	if !IsFileOpen(fileOpen) {
+		t.Errorf("IsFileOpen(fileOpen) -> true: false")
+	}
+
+	if IsFileOpen(fileClose) {
+		t.Errorf("IsFileOpen(fileClose) -> false: true")
+	}
+
+	if IsFileOpen(fileNil) {
+		t.Errorf("IsFileOpen(fileNil) -> false: true")
+	}
+}

+ 25 - 13
src/utils/formatutils/format.go

@@ -4,14 +4,21 @@
 
 package formatutils
 
-import "strings"
+import (
+	"strings"
+)
 
 const NormalConsoleWidth = 80
 
+// FormatTextToWidth 把文本控制在固定长度内(直接指定长度 width)
 func FormatTextToWidth(text string, width int) string {
 	return FormatTextToWidthAndPrefix(text, 0, width)
 }
 
+// FormatTextToWidthAndPrefix 把文本控制在固定的长度
+// prefixWidth 每行开头的空格
+// overallWidth 每行总长度
+// 实际每行字符数:width = overallWidth - prefixWidth
 func FormatTextToWidthAndPrefix(text string, prefixWidth int, overallWidth int) string {
 	var result strings.Builder
 
@@ -20,26 +27,30 @@ func FormatTextToWidthAndPrefix(text string, prefixWidth int, overallWidth int)
 		panic("bad width")
 	}
 
-	text = strings.ReplaceAll(text, "\r\n", "\n")
+	text = strings.TrimRight(strings.Replace(text, "\r", "", -1), "\n")
 
-	for _, line := range strings.Split(text, "\n") {
-		result.WriteString(strings.Repeat(" ", prefixWidth))
+LineCycle:
+	for _, line := range strings.Split(text, "\n") { // 逐行遍历
+		result.WriteString(strings.Repeat(" ", prefixWidth)) // 输出当前行的 prefix 空格
 
-		if line == "" {
+		if line == "" { // 如果当前行为空则直接换行返回
 			result.WriteString("\n")
-			continue
+			continue LineCycle
 		}
 
-		spaceCount := CountSpaceInStringPrefix(line) % width
 		newLineLength := 0
-		if spaceCount < 80 {
+
+		spaceCount := countSpaceInStringPrefix(line) % width // 获取当前行的开头空格,但空格数不超过单行字符总长度(有时很首行空格起到一定语法作用,因此需要保留,而下面的 for 循环使用 strings.Fields 分割字符串会导致空格被忽略,因此在此处要提前处理)
+		if spaceCount != 0 {
 			result.WriteString(strings.Repeat(" ", spaceCount))
 			newLineLength = spaceCount
 		}
 
-		for _, word := range strings.Fields(line) {
-			if newLineLength+len(word) >= width {
-				result.WriteString("\n")
+		line = strings.TrimSpace(line) // 空格已在上面处理,此处可以把空格删除
+
+		for _, word := range strings.Fields(line) { // 使用 strings.Fields 遍历每一行的每一个单词
+			if newLineLength+len(word) >= width { // 若写入新单词后超过单行总长度,则换行。
+				result.WriteString("\n") // 输出 "\n",并输出当前行的 prefix 空格。(从第二行开始、本循环中每增加一行,就要在这里写入 prefix 空格。而第一行的 prefix 空格则 LineCycle 一开始的时候写入)
 				result.WriteString(strings.Repeat(" ", prefixWidth))
 				newLineLength = 0
 			}
@@ -55,7 +66,7 @@ func FormatTextToWidthAndPrefix(text string, prefixWidth int, overallWidth int)
 		}
 
 		if newLineLength != 0 {
-			result.WriteString("\n")
+			result.WriteString("\n") // 写入最后的换行符
 			newLineLength = 0
 		}
 	}
@@ -63,7 +74,8 @@ func FormatTextToWidthAndPrefix(text string, prefixWidth int, overallWidth int)
 	return strings.TrimRight(result.String(), "\n")
 }
 
-func CountSpaceInStringPrefix(str string) int {
+// countSpaceInStringPrefix 计算字符串开头的空格数
+func countSpaceInStringPrefix(str string) int {
 	var res int
 	for _, r := range str {
 		if r == ' ' {

+ 30 - 0
src/utils/formatutils/format_test.go

@@ -0,0 +1,30 @@
+// 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 formatutils
+
+import (
+	_ "embed"
+	"github.com/SongZihuan/BackendServerTemplate/tool/utils/cleanstringutils"
+	"strings"
+	"testing"
+)
+
+//go:embed test_file_1.txt
+var testFileInput string
+
+//go:embed test_file_2.txt
+var testFileOutput string
+
+func init() {
+	testFileInput = cleanstringutils.GetString(testFileInput)
+	testFileOutput = strings.TrimRight(strings.Replace(testFileOutput, "\r", "\n", -1), "\n")
+}
+
+func TestFormat(t *testing.T) {
+	res := FormatTextToWidthAndPrefix(testFileInput, 10, 80)
+	if res != testFileOutput {
+		t.Errorf("format string error: \n===START EXPECTED===\n%s\n===END===\n===START ACTUAL===\n%s\n===END===\n", testFileOutput, res)
+	}
+}

+ 8 - 0
src/utils/formatutils/test_file_1.txt

@@ -0,0 +1,8 @@
+The MIT License (MIT)
+Copyright © 2025 宋子桓(Song Zihuan)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 21 - 0
src/utils/formatutils/test_file_2.txt

@@ -0,0 +1,21 @@
+          The MIT License (MIT)
+          Copyright © 2025 宋子桓(Song Zihuan)
+          
+          Permission is hereby granted, free of charge, to any person obtaining
+          a copy of this software and associated documentation files (the
+          “Software”), to deal in the Software without restriction,
+          including without limitation the rights to use, copy, modify, merge,
+          publish, distribute, sublicense, and/or sell copies of the Software,
+          and to permit persons to whom the Software is furnished to do so,
+          subject to the following conditions:
+          
+          The above copyright notice and this permission notice shall be
+          included in all copies or substantial portions of the Software.
+          
+          THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
+          EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+          MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+          IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+          CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+          TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+          SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 0 - 16
src/utils/reflectutils/reflect.go

@@ -1,16 +0,0 @@
-// 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 reflectutils
-
-import "reflect"
-
-func HasFieldByReflect(typ reflect.Type, fieldName string) bool {
-	for i := 0; i < typ.NumField(); i++ {
-		if typ.Field(i).Name == fieldName {
-			return true
-		}
-	}
-	return false
-}

+ 61 - 0
src/utils/reutils/regex_test.go

@@ -0,0 +1,61 @@
+// 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 reutils
+
+import "testing"
+
+func TestSemanticVersion(t *testing.T) {
+	if !IsSemanticVersion("0.0.0") {
+		t.Errorf("SemanticVersion test failed: 0.0.0 (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.0.0") {
+		t.Errorf("SemanticVersion test failed: 1.0.0 (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.2.3") {
+		t.Errorf("SemanticVersion test failed: 1.2.3 (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.0.0+dev") {
+		t.Errorf("SemanticVersion test failed: 1.0.0+dev (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.0.0+dev-123") {
+		t.Errorf("SemanticVersion test failed: 1.0.0+dev-123 (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.0.0+dev.123") {
+		t.Errorf("SemanticVersion test failed: 1.0.0+dev-123 (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.0.0+dev-123.abc") {
+		t.Errorf("SemanticVersion test failed: 1.0.0+dev-123-456 (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.0.0-123.456") {
+		t.Errorf("SemanticVersion test failed: 1.0.0-123-456 (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.0.0-123-456+dev") {
+		t.Errorf("SemanticVersion test failed: 1.0.0-123-456+dev (must be true, but return false)")
+	}
+
+	if !IsSemanticVersion("1.0.0-123-456+dev-127") {
+		t.Errorf("SemanticVersion test failed: 1.0.0-123-456+dev-127 (must be true, but return false)")
+	}
+
+	if IsSemanticVersion("v0.0.0") {
+		t.Errorf("SemanticVersion test failed: v0.0.0 (must be false, but return true)")
+	}
+
+	if IsSemanticVersion("1.0.0.0") {
+		t.Errorf("SemanticVersion test failed: 1.0.0.0 (must be false, but return true)")
+	}
+
+	if IsSemanticVersion("1.0.0-123+dev-234+prod") {
+		t.Errorf("SemanticVersion test failed: 1.0.0-123+dev-234+prod (must be false, but return true)")
+	}
+}

+ 33 - 0
src/utils/sliceutils/slice_test.go

@@ -0,0 +1,33 @@
+// 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 sliceutils
+
+import "testing"
+
+func TestCopySlice(t *testing.T) {
+	src := []int{1, 2}
+	dest := CopySlice(src)
+
+	if len(dest) != len(src) || dest[0] != src[0] || dest[1] != src[1] {
+		t.Errorf("copy slice failed: data does not match")
+	}
+
+	src[1] = 3
+	if dest[1] == 3 {
+		t.Errorf("copy slice failed: shared underlying array")
+	}
+}
+
+func TestSliceHasItem(t *testing.T) {
+	src := []int{1, 2}
+
+	if !SliceHasItem(src, 1) {
+		t.Errorf("SliceHasItem([]int{1, 2}, 1) -> true: false")
+	}
+
+	if SliceHasItem(src, 3) {
+		t.Errorf("SliceHasItem([]int{1, 2}, 3) -> false: true")
+	}
+}

+ 16 - 0
src/utils/strconvutils/number.go

@@ -0,0 +1,16 @@
+// 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 strconvutils
+
+import "strconv"
+
+func ParserInt(s string, base int, bitSize int) (i int64, err error) {
+	res, err := strconv.ParseInt(s, base, bitSize)
+	if err != nil {
+		return 0, err
+	}
+
+	return res, nil
+}

+ 21 - 0
src/utils/strconvutils/number_test.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 strconvutils
+
+import "testing"
+
+func TestParserInt(t *testing.T) {
+	if res, err := ParserInt("100", 10, 64); err != nil {
+		t.Errorf("ParserInt(100) error: %s", err.Error())
+	} else if res != 100 {
+		t.Errorf("ParserInt(100) -> 100: %v", res)
+	}
+
+	if res, err := ParserInt("abc", 10, 64); err == nil {
+		t.Errorf("ParserInt(abc) error: err is nil")
+	} else if res != 0 {
+		t.Errorf("ParserInt(abc) -> 0: %v", res)
+	}
+}

+ 36 - 41
src/utils/strconvutils/time.go

@@ -6,7 +6,6 @@ package strconvutils
 
 import (
 	"fmt"
-	"strconv"
 	"strings"
 	"time"
 )
@@ -21,115 +20,111 @@ func ReadTimeDurationPositive(str string) time.Duration {
 }
 
 func ReadTimeDuration(str string) time.Duration {
-	if str == "forever" || str == "none" {
-		return -1
-	}
-
 	if strings.HasSuffix(strings.ToUpper(str), "Y") {
 		numStr := str[:len(str)-1]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * 24 * 365 * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "year") {
 		numStr := str[:len(str)-4]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * 24 * 365 * time.Duration(num)
 	}
 
 	if strings.HasSuffix(strings.ToUpper(str), "M") {
 		numStr := str[:len(str)-1]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * 24 * 31 * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "month") {
 		numStr := str[:len(str)-5]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * 24 * 31 * time.Duration(num)
 	}
 
 	if strings.HasSuffix(strings.ToUpper(str), "W") {
 		numStr := str[:len(str)-1]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * 24 * 7 * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "week") {
 		numStr := str[:len(str)-4]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * 24 * 7 * time.Duration(num)
 	}
 
 	if strings.HasSuffix(strings.ToUpper(str), "D") {
 		numStr := str[:len(str)-1]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * 24 * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "day") {
 		numStr := str[:len(str)-3]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * 24 * time.Duration(num)
 	}
 
 	if strings.HasSuffix(strings.ToUpper(str), "H") {
 		numStr := str[:len(str)-1]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "hour") {
 		numStr := str[:len(str)-4]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Hour * time.Duration(num)
 	}
 
-	if strings.HasSuffix(strings.ToUpper(str), "Min") { // 不能用M,否则会和 Month 冲突
+	if strings.HasSuffix(strings.ToUpper(str), "MIN") { // 不能用M,否则会和 Month 冲突
 		numStr := str[:len(str)-3]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Minute * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "minute") {
 		numStr := str[:len(str)-6]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Minute * time.Duration(num)
 	}
 
 	if strings.HasSuffix(strings.ToUpper(str), "S") {
 		numStr := str[:len(str)-1]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Second * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "second") {
 		numStr := str[:len(str)-6]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Second * time.Duration(num)
 	}
 
 	if strings.HasSuffix(strings.ToUpper(str), "MS") {
 		numStr := str[:len(str)-2]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Millisecond * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "millisecond") {
 		numStr := str[:len(str)-11]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Millisecond * time.Duration(num)
 	}
 
-	if strings.HasSuffix(strings.ToUpper(str), "MiS") { // 不能用 MS , 否则会和 millisecond 冲突
+	if strings.HasSuffix(strings.ToUpper(str), "MIS") { // 不能用 MS , 否则会和 millisecond 冲突
 		numStr := str[:len(str)-3]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Microsecond * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToUpper(str), "MicroS") {
 		numStr := str[:len(str)-6]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Microsecond * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "microsecond") {
 		numStr := str[:len(str)-11]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Microsecond * time.Duration(num)
 	}
 
 	if strings.HasSuffix(strings.ToUpper(str), "NS") {
 		numStr := str[:len(str)-2]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Nanosecond * time.Duration(num)
 	} else if strings.HasSuffix(strings.ToLower(str), "nanosecond") {
 		numStr := str[:len(str)-10]
-		num, _ := strconv.ParseInt(numStr, 10, 64)
+		num, _ := ParserInt(numStr, 10, 64)
 		return time.Nanosecond * time.Duration(num)
 	}
 
-	num, _ := strconv.ParseInt(str, 10, 64)
+	num, _ := ParserInt(str, 10, 64)
 	return time.Duration(num) * time.Second
 }
 
@@ -137,15 +132,15 @@ func TimeDurationToStringCN(t time.Duration) string {
 	const day = 24 * time.Hour
 	const year = 365 * day
 
-	if t > year {
+	if t >= year {
 		return fmt.Sprintf("%d年", t/year)
-	} else if t > day {
+	} else if t >= day {
 		return fmt.Sprintf("%d天", t/day)
-	} else if t > time.Hour {
+	} else if t >= time.Hour {
 		return fmt.Sprintf("%d小时", t/time.Hour)
-	} else if t > time.Minute {
+	} else if t >= time.Minute {
 		return fmt.Sprintf("%d分钟", t/time.Minute)
-	} else if t > time.Second {
+	} else if t >= time.Second {
 		return fmt.Sprintf("%d秒", t/time.Second)
 	}
 
@@ -156,15 +151,15 @@ func TimeDurationToString(t time.Duration) string {
 	const day = 24 * time.Hour
 	const year = 365 * day
 
-	if t > year {
-		return fmt.Sprintf("%dY", t/year)
-	} else if t > day {
-		return fmt.Sprintf("%dD", t/day)
-	} else if t > time.Hour {
+	if t >= year {
+		return fmt.Sprintf("%dy", t/year)
+	} else if t >= day {
+		return fmt.Sprintf("%dd", t/day)
+	} else if t >= time.Hour {
 		return fmt.Sprintf("%dh", t/time.Hour)
-	} else if t > time.Minute {
+	} else if t >= time.Minute {
 		return fmt.Sprintf("%dmin", t/time.Minute)
-	} else if t > time.Second {
+	} else if t >= time.Second {
 		return fmt.Sprintf("%ds", t/time.Second)
 	}
 

+ 42 - 0
src/utils/strconvutils/time_test.go

@@ -0,0 +1,42 @@
+// 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 strconvutils
+
+import (
+	"testing"
+	"time"
+)
+
+func TestReadTimeDuration(t *testing.T) {
+	if res := ReadTimeDuration("10Min"); res != 10*time.Minute {
+		t.Errorf("ReadTimeDuration(10Min) error")
+	}
+
+	if res := ReadTimeDuration("-10S"); res != -10*time.Second {
+		t.Errorf("ReadTimeDuration(-10S) error")
+	}
+
+	if res := ReadTimeDurationPositive("-10S"); res != 0 {
+		t.Errorf("ReadTimeDurationPositive(-10S) error")
+	}
+}
+
+func TestTimeDurationToString(t *testing.T) {
+	if res := TimeDurationToString(5 * 24 * time.Hour); res != "5d" {
+		t.Errorf("TimeDurationToString(5*24*Hour) -> 5d: %s", res)
+	}
+
+	if res := TimeDurationToString(3 * time.Hour); res != "3h" {
+		t.Errorf("TimeDurationToString(3*Hour) -> 3h: %s", res)
+	}
+
+	if res := TimeDurationToStringCN(5 * 24 * time.Hour); res != "5天" {
+		t.Errorf("TimeDurationToString(5*24*Hour) -> 5天: %s", res)
+	}
+
+	if res := TimeDurationToStringCN(3 * time.Hour); res != "3小时" {
+		t.Errorf("TimeDurationToString(3*Hour) -> 3小时: %s", res)
+	}
+}

+ 1 - 1
src/utils/timeutils/local_timezone_posix.go

@@ -16,7 +16,7 @@ import (
 	"time"
 )
 
-func LoadLocation(name string) (*time.Location, error) {
+func LoadTimezone(name string) (*time.Location, error) {
 	return time.LoadLocation(name)
 }
 

+ 1 - 1
src/utils/timeutils/local_timezone_win32.go

@@ -14,7 +14,7 @@ import (
 	"time"
 )
 
-func LoadLocation(name string) (*time.Location, error) {
+func LoadTimezone(name string) (*time.Location, error) {
 	loc, err1 := time.LoadLocation(name)
 	if err1 == nil && loc != nil {
 		return loc, nil

+ 50 - 0
src/utils/timeutils/timezone_test.go

@@ -0,0 +1,50 @@
+// 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 timeutils
+
+import (
+	"runtime"
+	"testing"
+)
+
+func TestLocalLocation(t *testing.T) {
+	if res := GetLocalTimezone(); res == nil {
+		t.Errorf("GetLocalTimezone error: res is nil")
+	} else if res.String() == "Local" {
+		t.Errorf("GetLocalTimezone error: res.String() is %s", res.String())
+	}
+}
+
+func TestLoadLocation(t *testing.T) {
+	if res, err := LoadTimezone("UTC"); err != nil {
+		t.Errorf("LoadTimezone(UTC) error: %s", err.Error())
+	} else if res == nil {
+		t.Errorf("LoadTimezone(UTC) error: res is nil")
+	} else if res.String() != "UTC" {
+		t.Errorf("LoadTimezone(UTC) error: res.String() is %s", res.String())
+	}
+
+	if res, err := LoadTimezone("Asia/Shanghai"); err != nil {
+		t.Errorf("LoadTimezone(Asia/Shanghai) error: %s", err.Error())
+	} else if res == nil {
+		t.Errorf("LoadTimezone(Asia/Shanghai) error: res is nil")
+	} else if res.String() != "Asia/Shanghai" {
+		t.Errorf("LoadTimezone(Asia/Shanghai) error: res.String() is %s", res.String())
+	}
+}
+
+func TestLoadLocationWin32(t *testing.T) {
+	if runtime.GOOS != "windows" {
+		t.Skipf("OS is not windows")
+	}
+
+	if res, err := LoadTimezone("China Standard Time"); err != nil {
+		t.Errorf("LoadTimezone(China Standard Time) error: %s", err.Error())
+	} else if res == nil {
+		t.Errorf("LoadTimezone(China Standard Time) error: res is nil")
+	} else if res.String() != "Asia/Shanghai" {
+		t.Errorf("LoadTimezone(China Standard Time) error: res.String() is %s", res.String())
+	}
+}

+ 0 - 14
src/utils/typeutils/stringbool.go

@@ -64,20 +64,6 @@ func (s *StringBool) ToString() string {
 	return string(disable)
 }
 
-func (s *StringBool) ToStringDefaultEnable() string {
-	if s.IsEnable(true) {
-		return string(enable)
-	}
-	return string(disable)
-}
-
-func (s *StringBool) ToStringDefaultDisable() string {
-	if s.IsEnable(false) {
-		return string(enable)
-	}
-	return string(disable)
-}
-
 func (s *StringBool) ToBool(defaultVal ...bool) bool {
 	return s.IsEnable(defaultVal...)
 }

+ 88 - 0
src/utils/typeutils/stringbool_test.go

@@ -0,0 +1,88 @@
+// 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 typeutils
+
+import "testing"
+
+func TestStringBool(t *testing.T) {
+	t.Run("enable default", func(t *testing.T) {
+		var res StringBool
+		if res.IsEnable() {
+			t.Errorf("res.IsEnable() -> false: true")
+		}
+	})
+
+	t.Run("enable default", func(t *testing.T) {
+		var res StringBool
+		if res.IsDisable() {
+			t.Errorf("res.IsDisable() -> false: true")
+		}
+	})
+
+	t.Run("enable default true", func(t *testing.T) {
+		var res StringBool
+		if !res.IsEnable(true) {
+			t.Errorf("res.IsEnable(true) -> true: false")
+		}
+	})
+
+	t.Run("enable default false", func(t *testing.T) {
+		var res StringBool
+		if res.IsEnable(false) {
+			t.Errorf("res.IsEnable(false) -> false: true")
+		}
+	})
+
+	t.Run("disable default true", func(t *testing.T) {
+		var res StringBool
+		if !res.IsDisable(true) {
+			t.Errorf("res.IsDisable(true) -> true: false")
+		}
+	})
+
+	t.Run("disable default false", func(t *testing.T) {
+		var res StringBool
+		if res.IsDisable(false) {
+			t.Errorf("res.IsDisable(false) -> false: true")
+		}
+	})
+
+	t.Run("set default disable", func(t *testing.T) {
+		var res StringBool
+		res.SetDefaultDisable()
+
+		if !res.IsDisable() {
+			t.Errorf("res.IsDisable() -> true: false")
+		}
+	})
+
+	t.Run("set default enable", func(t *testing.T) {
+		var res StringBool
+		res.SetDefaultEnable()
+
+		if !res.IsEnable() {
+			t.Errorf("res.IsEnable() -> true: false")
+		}
+	})
+
+	t.Run("set default double", func(t *testing.T) {
+		var res StringBool
+		res.SetDefaultEnable()
+		res.SetDefaultDisable()
+
+		if !res.IsEnable() {
+			t.Errorf("res.IsEnable() -> true: false")
+		}
+	})
+
+	t.Run("to bool", func(t *testing.T) {
+		var res StringBool
+		res.SetDefaultEnable()
+
+		if res.ToBool() != true {
+			t.Errorf("res.ToBool() -> true: false")
+		}
+	})
+}

+ 6 - 0
third-party/github.com.SongZihuan.BackendServerTemplate/CHANGELOG.md

@@ -4,6 +4,12 @@
 
 其格式基于 [CHANGELOG 准则](./CHANGELOG_SPECIFICATION.md) 。
 
+## [未发布]
+
+### 新增
+
+- 添加`utils`包的单元测试。
+
 ## [0.13.0] - 2025/04/28 Asia/Shanghai
 
 ### 新增