From 113d6d944281c9485272e2fad581aeec25c702f4 Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Thu, 4 Jun 2026 23:51:40 +0800 Subject: [PATCH 1/8] refactor: lazy config loading in task CLI and absolute path resolution --- cmd/server/main.go | 2 +- cmd/task/main.go | 78 +++++++++++++++++++------- cmd/task/root.go | 19 ++++--- cmd/task/save_page_data.go | 11 +++- cmd/task/save_page_data_cron.go | 11 +++- internal/pkg/confpath/confpath.go | 62 ++++++++++++++++++-- internal/pkg/confpath/confpath_test.go | 66 +++++++++++++++++++++- internal/server/ping_http.go | 2 +- test/e2e/ping_test.go | 2 +- 9 files changed, 208 insertions(+), 45 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 2c3e6bd..74d16ea 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -34,7 +34,7 @@ var ( func init() { flag.StringVar(&Flagconfig, "conf", "./config", "config directory, eg: -conf ./config") - flag.StringVar(&Flagenv, "e", "", "env shorthand: local, prod, integration ??config/{env}") + flag.StringVar(&Flagenv, "e", "", "env shorthand: local, prod, integration → config/{env}") } func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App { diff --git a/cmd/task/main.go b/cmd/task/main.go index dc3c3a8..2e74373 100644 --- a/cmd/task/main.go +++ b/cmd/task/main.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "os" @@ -14,46 +13,83 @@ import ( "github.com/go-kratos/kratos/v2/log" ) -var ( - flagconf string - flagenv string -) - func init() { - flag.StringVar(&flagconf, "conf", "./config", "config directory, eg: -conf ./config") - flag.StringVar(&flagenv, "e", "", "env shorthand (Gin parity): local, prod, integration ??config/{env}") + // Register flags on rootCmd so cobra recognizes them + rootCmd.PersistentFlags().StringVarP(&flagconf, "conf", "c", "./config", "config directory, eg: -conf ./config") + rootCmd.PersistentFlags().StringVarP(&flagenv, "e", "e", "", "env shorthand (Gin parity): local, prod, integration → config/{env}") rootCmd.PersistentFlags().StringVar(&flagenv, "env", "", "alias for -e") + + // Register subcommands + rootCmd.AddCommand(newSavePageDataCmd()) + rootCmd.AddCommand(newSavePageDataCronCmd()) } func main() { - flag.Parse() + // Normalize go-style flags to cobra format before cobra parses. + // Cobra treats -conf as -c + "onfig", so we convert -conf → --conf upfront. + args := make([]string, 0, len(os.Args)-1) + for i := 1; i < len(os.Args); i++ { + arg := os.Args[i] + if arg == "-conf" { + arg = "--conf" + } + args = append(args, arg) + } + + // If no subcommand provided, default to savePageData + hasSubcommand := false + for _, arg := range args { + if arg == "savePageData" || arg == "savePageDataCron" || arg == "completion" || arg == "help" { + hasSubcommand = true + break + } + } + if !hasSubcommand { + args = append(args, "savePageData") + } + + os.Args = append([]string{os.Args[0]}, args...) + + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +// loadBootstrap loads config and returns bootstrap + cleanup function. +// Subcommands should call this in their RunE. +func loadBootstrap() (*conf.Bootstrap, func(), error) { confDir := confpath.Resolve(flagconf, flagenv) if flagenv != "" { - fmt.Printf("using config env %q ??%s\n", flagenv, confDir) + fmt.Printf("using config env %q → %s\n", flagenv, confDir) } logger := log.NewStdLogger(os.Stdout) c := config.New(config.WithSource(file.NewSource(confDir))) - defer c.Close() if err := c.Load(); err != nil { - panic(err) + c.Close() + return nil, nil, err } var bc conf.Bootstrap if err := c.Scan(&bc); err != nil { - panic(err) + c.Close() + return nil, nil, err } cleanup, err := initTaskApp(bc.GetData(), logger) if err != nil { - panic(err) + c.Close() + return nil, nil, err } - defer cleanup() - - crawlerUC := biz.NewCrawlerUsecase(bc.GetApp()) - rootCmd.AddCommand(newSavePageDataCmd(crawlerUC)) - rootCmd.AddCommand(newSavePageDataCronCmd(crawlerUC)) - if err := rootCmd.Execute(); err != nil { - os.Exit(1) + fullCleanup := func() { + cleanup() + c.Close() } + + return &bc, fullCleanup, nil +} + +// createCrawlerUsecase is called by subcommands after loading config. +func createCrawlerUsecase(bc *conf.Bootstrap) *biz.CrawlerUsecase { + return biz.NewCrawlerUsecase(bc.GetApp()) } diff --git a/cmd/task/root.go b/cmd/task/root.go index d775674..f7f4e80 100644 --- a/cmd/task/root.go +++ b/cmd/task/root.go @@ -1,11 +1,14 @@ package main -import ( - "github.com/spf13/cobra" -) +import "github.com/spf13/cobra" + +var ( + flagconf string + flagenv string -var rootCmd = &cobra.Command{ - Use: "kratos-task", - Short: "Kratos develop template CLI tasks", - Long: "Scheduled / batch tasks migrated from gin-develop-template.", -} + rootCmd = &cobra.Command{ + Use: "kratos-task", + Short: "Kratos develop template CLI tasks", + Long: "Scheduled / batch tasks migrated from gin-develop-template.", + } +) diff --git a/cmd/task/save_page_data.go b/cmd/task/save_page_data.go index a173667..adb399e 100644 --- a/cmd/task/save_page_data.go +++ b/cmd/task/save_page_data.go @@ -1,17 +1,22 @@ package main import ( - "github.com/JasonMetal/kratos-develop-template/internal/biz" - "github.com/spf13/cobra" ) -func newSavePageDataCmd(uc *biz.CrawlerUsecase) *cobra.Command { +func newSavePageDataCmd() *cobra.Command { return &cobra.Command{ Use: "savePageData", Short: "爬数据入库(单次 GetAllTagsData)", Long: "与 gin-develop-template 的 savePageData 一致:只跑 GetAllTagsData,不执行 shell/cron", RunE: func(cmd *cobra.Command, args []string) error { + bc, cleanup, err := loadBootstrap() + if err != nil { + return err + } + defer cleanup() + + uc := createCrawlerUsecase(bc) return uc.RunSavePageData(cmd.Context()) }, } diff --git a/cmd/task/save_page_data_cron.go b/cmd/task/save_page_data_cron.go index ef07706..9fe9c28 100644 --- a/cmd/task/save_page_data_cron.go +++ b/cmd/task/save_page_data_cron.go @@ -6,18 +6,23 @@ import ( "os/signal" "syscall" - "github.com/JasonMetal/kratos-develop-template/internal/biz" - "github.com/spf13/cobra" ) -func newSavePageDataCronCmd(uc *biz.CrawlerUsecase) *cobra.Command { +func newSavePageDataCronCmd() *cobra.Command { var daemon bool cmd := &cobra.Command{ Use: "savePageDataCron", Short: "爬数据入库", Long: "抓取并入库(单次运行)。--daemon 与 gin crawlerLogic 一样按 cron 循环", RunE: func(cmd *cobra.Command, args []string) error { + bc, cleanup, err := loadBootstrap() + if err != nil { + return err + } + defer cleanup() + + uc := createCrawlerUsecase(bc) if daemon { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() diff --git a/internal/pkg/confpath/confpath.go b/internal/pkg/confpath/confpath.go index eecdeb4..78e5002 100644 --- a/internal/pkg/confpath/confpath.go +++ b/internal/pkg/confpath/confpath.go @@ -1,14 +1,68 @@ package confpath -import "path/filepath" +import ( + "os" + "path/filepath" + "strings" +) // Resolve picks the config directory (Gin -e local|prod parity via -conf or -e). +// Always returns an absolute path — safe for kratos run which changes working directory. func Resolve(confDir, env string) string { if env != "" { - return filepath.Join("config", env) + // If confDir was provided, join env with it + if confDir != "" { + return makeAbsolute(filepath.Join(confDir, env)) + } + // Default: config/{env} relative to project root + return makeAbsolute(filepath.Join("config", env)) } if confDir == "" { - return "./config" + return makeAbsolute("./config") } - return confDir + return makeAbsolute(confDir) +} + +// makeAbsolute converts a relative path to absolute by finding the project root (go.mod). +func makeAbsolute(path string) string { + if filepath.IsAbs(path) { + return path + } + // Try cwd first + if _, err := os.Stat(path); err == nil { + abs, err := filepath.Abs(path) + if err == nil { + return abs + } + } + // Fallback: find project root by searching for go.mod + root := findProjectRoot() + if root != "" { + candidate := filepath.Join(root, path) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + // Last resort: just make it absolute from cwd + abs, _ := filepath.Abs(path) + return abs +} + +// findProjectRoot searches upward for go.mod (project root). +func findProjectRoot() string { + dir, err := os.Getwd() + if err != nil { + return "" + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir || strings.HasSuffix(dir, string(filepath.Separator)) { + break + } + dir = parent + } + return "" } diff --git a/internal/pkg/confpath/confpath_test.go b/internal/pkg/confpath/confpath_test.go index 7f02fc0..e07f1fb 100644 --- a/internal/pkg/confpath/confpath_test.go +++ b/internal/pkg/confpath/confpath_test.go @@ -1,16 +1,76 @@ package confpath import ( + "os" "path/filepath" + "strings" "testing" ) func TestResolve(t *testing.T) { - want := filepath.Join("config", "local") + // Resolve now returns absolute paths + // Find project root (where go.mod is) + projectRoot := findProjectRoot() + if projectRoot == "" { + t.Fatal("could not find project root") + } + + want := filepath.Join(projectRoot, "config", "local") if got := Resolve("./config", "local"); got != want { t.Fatalf("got %q want %q", got, want) } - if got := Resolve("/etc/app", ""); got != "/etc/app" { - t.Fatalf("got %q", got) + + // Absolute path should remain absolute + got := Resolve("", "") + if !filepath.IsAbs(got) { + t.Fatalf("expected absolute path, got %q", got) + } + + // Empty inputs should return absolute config path + wantConfig := filepath.Join(projectRoot, "config") + if got := Resolve("", ""); got != wantConfig { + t.Fatalf("got %q want %q", got, wantConfig) + } +} + +func TestResolveEnv(t *testing.T) { + projectRoot := findProjectRoot() + if projectRoot == "" { + t.Fatal("could not find project root") + } + + // Test env-based resolution + want := filepath.Join(projectRoot, "config", "prod") + if got := Resolve("", "prod"); got != want { + t.Fatalf("got %q want %q", got, want) + } +} + +func TestMakeAbsolute(t *testing.T) { + // Already absolute paths should be unchanged + absPath := "D:\\etc\\app" + if got := makeAbsolute(absPath); got != absPath { + t.Fatalf("got %q want %s", got, absPath) + } + + // Relative paths should become absolute + got := makeAbsolute("./config") + if !filepath.IsAbs(got) { + t.Fatalf("expected absolute path, got %q", got) + } + if !strings.HasSuffix(got, "config") { + t.Fatalf("expected path to end with 'config', got %q", got) + } +} + +func TestFindProjectRoot(t *testing.T) { + root := findProjectRoot() + if root == "" { + t.Fatal("expected to find project root") + } + + // Verify go.mod exists in returned directory + if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil { + t.Fatalf("go.mod not found in %s", root) } } diff --git a/internal/server/ping_http.go b/internal/server/ping_http.go index adf487d..32f456c 100644 --- a/internal/server/ping_http.go +++ b/internal/server/ping_http.go @@ -13,6 +13,6 @@ func RegisterPing(srv *khttp.Server) { w := ctx.Response() w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) - return json.NewEncoder(w).Encode("Health check for Gin framework:init pong") + return json.NewEncoder(w).Encode("Health check for Kratos framework: pong") }) } diff --git a/test/e2e/ping_test.go b/test/e2e/ping_test.go index 522b7f1..24ffe6a 100644 --- a/test/e2e/ping_test.go +++ b/test/e2e/ping_test.go @@ -33,7 +33,7 @@ func TestPingRoute(t *testing.T) { if err := json.Unmarshal(body, &s); err != nil { t.Fatalf("body %q: %v", body, err) } - want := "Health check for Gin framework:init pong" + want := "Health check for Kratos framework: pong" if s != want { t.Fatalf("got %q want %q", s, want) } From b68f04260a63223b7a24710d1c7e5bdd42ab3716 Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Thu, 4 Jun 2026 23:51:40 +0800 Subject: [PATCH 2/8] refactor: lazy config loading in task CLI and absolute path resolution --- configs/dev/config.yaml | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 configs/dev/config.yaml diff --git a/configs/dev/config.yaml b/configs/dev/config.yaml new file mode 100644 index 0000000..faf54d7 --- /dev/null +++ b/configs/dev/config.yaml @@ -0,0 +1,56 @@ +# Local dev (Gin: -e local → config/local/) +# Run: go run ./cmd/server/ -e local +# go run ./cmd/task/ -e local savePageData + +server: + http: + addr: 0.0.0.0:8989 + timeout: 10s + grpc: + addr: 0.0.0.0:50052 + timeout: 10s +data: + database: + enabled: false + driver: mysql + source: root:root@tcp(127.0.0.1:3306)/gin_db?charset=utf8&parseTime=True&loc=Local&timeout=10s + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: false + addr: 127.0.0.1:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: false + brokers: + - 127.0.0.1:9092 + username: "" + password: "" + algorithm: "" + rabbitmq: + enabled: false + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: my_exchange + exchange_type: direct + routing_key: my_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "foobar386cb6d14ab7b5f6738f7fc1704cto" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: output + api_host: http://127.0.0.1:8888 From ab62f4e7c62386acb6e619b8e32a8a7045df69cc Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Thu, 4 Jun 2026 23:57:04 +0800 Subject: [PATCH 3/8] chore: add environment config files and untrack from gitignore --- .gitignore | 6 +--- config/bvt/config.yaml | 52 +++++++++++++++++++++++++++++++ config/integration/config.yaml | 34 +++++++++++++++++++++ config/local/config.yaml | 56 ++++++++++++++++++++++++++++++++++ config/prod/config.yaml | 55 +++++++++++++++++++++++++++++++++ config/test/config.yaml | 55 +++++++++++++++++++++++++++++++++ 6 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 config/bvt/config.yaml create mode 100644 config/integration/config.yaml create mode 100644 config/local/config.yaml create mode 100644 config/prod/config.yaml create mode 100644 config/test/config.yaml diff --git a/.gitignore b/.gitignore index 456778e..381978f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,12 +15,8 @@ config/certs/*.pem config/certs/*.zip # Local config (may contain secrets) +# NOTE: environment configs are now tracked; only config.yaml (potential secrets) is ignored config/config.yaml -config/local/ -config/bvt/ -config/test/ -config/prod/ -config/integration/ # Dependencies vendor/ diff --git a/config/bvt/config.yaml b/config/bvt/config.yaml new file mode 100644 index 0000000..e1e903a --- /dev/null +++ b/config/bvt/config.yaml @@ -0,0 +1,52 @@ +# BVT env (Gin: -e bvt → config/bvt/) +# Run: go run ./cmd/server/ -e bvt + +server: + http: + addr: 0.0.0.0:8989 + timeout: 15s + grpc: + addr: 0.0.0.0:50052 + timeout: 15s +data: + database: + enabled: false + driver: mysql + source: root:root@tcp(127.0.0.1:3306)/gin_db?charset=utf8&parseTime=True&loc=Local&timeout=10s + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: false + addr: 127.0.0.1:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: false + brokers: + - 127.0.0.1:9092 + rabbitmq: + enabled: false + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: bvt_exchange + exchange_type: direct + routing_key: bvt_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "foobar386cb6d14ab7b5f6738f7fc1704cto" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: output + api_host: http://127.0.0.1:8888 diff --git a/config/integration/config.yaml b/config/integration/config.yaml new file mode 100644 index 0000000..e16ec8c --- /dev/null +++ b/config/integration/config.yaml @@ -0,0 +1,34 @@ +# Integration test profile — isolated from config/config.yaml +# go test -tags=integration ./test/integration/... +server: + http: + addr: 0.0.0.0:8989 + timeout: 30s + grpc: + addr: 0.0.0.0:50052 + timeout: 30s +data: + database: + enabled: false + redis: + enabled: false + kafka: + enabled: true + brokers: + - 127.0.0.1:9092 + rabbitmq: + enabled: true + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: kratos_test_exchange + exchange_type: direct + routing_key: kratos_test_routing + durable: true +app: + auth: + domain_mark: "" + auth_token: "integration-test-token" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 diff --git a/config/local/config.yaml b/config/local/config.yaml new file mode 100644 index 0000000..faf54d7 --- /dev/null +++ b/config/local/config.yaml @@ -0,0 +1,56 @@ +# Local dev (Gin: -e local → config/local/) +# Run: go run ./cmd/server/ -e local +# go run ./cmd/task/ -e local savePageData + +server: + http: + addr: 0.0.0.0:8989 + timeout: 10s + grpc: + addr: 0.0.0.0:50052 + timeout: 10s +data: + database: + enabled: false + driver: mysql + source: root:root@tcp(127.0.0.1:3306)/gin_db?charset=utf8&parseTime=True&loc=Local&timeout=10s + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: false + addr: 127.0.0.1:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: false + brokers: + - 127.0.0.1:9092 + username: "" + password: "" + algorithm: "" + rabbitmq: + enabled: false + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: my_exchange + exchange_type: direct + routing_key: my_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "foobar386cb6d14ab7b5f6738f7fc1704cto" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: output + api_host: http://127.0.0.1:8888 diff --git a/config/prod/config.yaml b/config/prod/config.yaml new file mode 100644 index 0000000..fb68fcf --- /dev/null +++ b/config/prod/config.yaml @@ -0,0 +1,55 @@ +# Production example (Gin: -e prod → config/prod/) +# Run: go run ./cmd/server/ -e prod + +server: + http: + addr: 0.0.0.0:8989 + timeout: 30s + grpc: + addr: 0.0.0.0:50052 + timeout: 30s +data: + database: + enabled: true + driver: mysql + source: user:pass@tcp(mysql:3306)/app_db?charset=utf8mb4&parseTime=True&loc=Local + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: true + addr: redis:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: true + brokers: + - kafka:9092 + username: "" + password: "" + algorithm: "" + rabbitmq: + enabled: true + url: amqp://guest:guest@rabbitmq:5672/ + exchange: my_exchange + exchange_type: direct + routing_key: my_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "change-me-in-production" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: /var/data/crawler + api_host: http://127.0.0.1:8888 diff --git a/config/test/config.yaml b/config/test/config.yaml new file mode 100644 index 0000000..37f8e5e --- /dev/null +++ b/config/test/config.yaml @@ -0,0 +1,55 @@ +# Test env (Gin: -e test → config/test/) +# Run: go run ./cmd/server/ -e test + +server: + http: + addr: 0.0.0.0:8989 + timeout: 10s + grpc: + addr: 0.0.0.0:50052 + timeout: 10s +data: + database: + enabled: false + driver: mysql + source: root:root@tcp(127.0.0.1:3306)/gin_db?charset=utf8&parseTime=True&loc=Local&timeout=10s + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: false + addr: 127.0.0.1:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: false + brokers: + - 127.0.0.1:9092 + username: "" + password: "" + algorithm: "" + rabbitmq: + enabled: false + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: my_exchange + exchange_type: direct + routing_key: my_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "foobar386cb6d14ab7b5f6738f7fc1704cto" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: output + api_host: http://127.0.0.1:8888 From 89448a85de6da6b2e80f399a93c2191ca2c24cfe Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Thu, 4 Jun 2026 23:59:08 +0800 Subject: [PATCH 4/8] chore: remove redundant configs/ directory, use config/ instead --- configs/dev/config.yaml | 56 ----------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 configs/dev/config.yaml diff --git a/configs/dev/config.yaml b/configs/dev/config.yaml deleted file mode 100644 index faf54d7..0000000 --- a/configs/dev/config.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Local dev (Gin: -e local → config/local/) -# Run: go run ./cmd/server/ -e local -# go run ./cmd/task/ -e local savePageData - -server: - http: - addr: 0.0.0.0:8989 - timeout: 10s - grpc: - addr: 0.0.0.0:50052 - timeout: 10s -data: - database: - enabled: false - driver: mysql - source: root:root@tcp(127.0.0.1:3306)/gin_db?charset=utf8&parseTime=True&loc=Local&timeout=10s - max_idle_conns: 10 - max_open_conns: 100 - conn_max_lifetime: 3600s - redis: - enabled: false - addr: 127.0.0.1:6379 - password: "" - db: 0 - dial_timeout_seconds: 5 - read_timeout_seconds: 3 - write_timeout_seconds: 3 - pool_size: 50 - min_idle_conns: 10 - kafka: - enabled: false - brokers: - - 127.0.0.1:9092 - username: "" - password: "" - algorithm: "" - rabbitmq: - enabled: false - url: amqp://guest:guest@127.0.0.1:5672/ - exchange: my_exchange - exchange_type: direct - routing_key: my_routing_key - durable: true -app: - auth: - domain_mark: "" - auth_token: "foobar386cb6d14ab7b5f6738f7fc1704cto" - private_key_path: config/certs/privateKey.pem - public_key_path: config/certs/publicKey.pem - rsa: - certs_dir: config/certs - key_bits: 2048 - crawler: - shell_script: scripts/crawler.sh - output_dir: output - api_host: http://127.0.0.1:8888 From a89b8b41df6f891bfe4ab5b1796f4bcb92dfede9 Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Fri, 5 Jun 2026 00:02:36 +0800 Subject: [PATCH 5/8] update: configs --- .gitignore | 6 +-- README.md | 14 +++++- cmd/server/main.go | 2 +- cmd/task/main.go | 2 +- cmd/task/output/outputGetAllTagsData.dat | 1 + {config => configs}/bvt/config.yaml | 0 {config => configs}/config.example.yaml | 0 {config => configs}/integration/config.yaml | 0 {config => configs}/local/config.yaml | 0 {config => configs}/prod/config.yaml | 0 {config => configs}/test/config.yaml | 0 internal/pkg/confpath/confpath.go | 53 +++++++++++++++++++-- internal/pkg/confpath/confpath_test.go | 20 ++++---- output/outputGetAllTagsData.dat | 1 + 14 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 cmd/task/output/outputGetAllTagsData.dat rename {config => configs}/bvt/config.yaml (100%) rename {config => configs}/config.example.yaml (100%) rename {config => configs}/integration/config.yaml (100%) rename {config => configs}/local/config.yaml (100%) rename {config => configs}/prod/config.yaml (100%) rename {config => configs}/test/config.yaml (100%) create mode 100644 output/outputGetAllTagsData.dat diff --git a/.gitignore b/.gitignore index 381978f..554f315 100644 --- a/.gitignore +++ b/.gitignore @@ -11,12 +11,12 @@ bin/ .qwen/skills/ # Local certs (generated) -config/certs/*.pem -config/certs/*.zip +configs/certs/*.pem +configs/certs/*.zip # Local config (may contain secrets) # NOTE: environment configs are now tracked; only config.yaml (potential secrets) is ignored -config/config.yaml +configs/config.yaml # Dependencies vendor/ diff --git a/README.md b/README.md index b12384e..d965261 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,21 @@ make generate && make wire && make build # 运行(默认 MySQL/Redis/Kafka/RabbitMQ 均为 enabled:false,无需依赖即可启动) ./bin/server -conf=./config +# 指定环境配置(-e 或 --env,加载 config/{env}/config.yaml) +./bin/server -e local +./bin/server -e prod + # CLI 定时任务 go build -o bin/task ./cmd/task/ -./bin/task savePageDataCron -conf=./config + +# 交互命令(cobra CLI,支持 -conf/-c 指定配置目录,-e 指定环境) +./bin/task help # 查看帮助 +./bin/task # 默认执行 savePageData +./bin/task savePageData # 爬数据入库(单次) +./bin/task savePageData -e local # 使用 local 环境配置 +./bin/task savePageDataCron # 爬数据入库(单次运行) +./bin/task savePageDataCron --daemon # 按 cron 循环运行 +./bin/task savePageDataCron -e prod --daemon # 生产环境循环爬取 ``` 默认 HTTP 端口 `8989`(与原 Gin 项目一致),Hello 接口:`GET http://127.0.0.1:8989/test` diff --git a/cmd/server/main.go b/cmd/server/main.go index 74d16ea..05c81eb 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -33,7 +33,7 @@ var ( ) func init() { - flag.StringVar(&Flagconfig, "conf", "./config", "config directory, eg: -conf ./config") + flag.StringVar(&Flagconfig, "conf", "./configs", "config directory, eg: -conf ./configs") flag.StringVar(&Flagenv, "e", "", "env shorthand: local, prod, integration → config/{env}") } diff --git a/cmd/task/main.go b/cmd/task/main.go index 2e74373..6d8441f 100644 --- a/cmd/task/main.go +++ b/cmd/task/main.go @@ -15,7 +15,7 @@ import ( func init() { // Register flags on rootCmd so cobra recognizes them - rootCmd.PersistentFlags().StringVarP(&flagconf, "conf", "c", "./config", "config directory, eg: -conf ./config") + rootCmd.PersistentFlags().StringVarP(&flagconf, "conf", "c", "./configs", "config directory, eg: -conf ./configs") rootCmd.PersistentFlags().StringVarP(&flagenv, "e", "e", "", "env shorthand (Gin parity): local, prod, integration → config/{env}") rootCmd.PersistentFlags().StringVar(&flagenv, "env", "", "alias for -e") diff --git a/cmd/task/output/outputGetAllTagsData.dat b/cmd/task/output/outputGetAllTagsData.dat new file mode 100644 index 0000000..7781def --- /dev/null +++ b/cmd/task/output/outputGetAllTagsData.dat @@ -0,0 +1 @@ +complete the task GetAllTagsData at 2026-06-04T23:52:58+08:00 diff --git a/config/bvt/config.yaml b/configs/bvt/config.yaml similarity index 100% rename from config/bvt/config.yaml rename to configs/bvt/config.yaml diff --git a/config/config.example.yaml b/configs/config.example.yaml similarity index 100% rename from config/config.example.yaml rename to configs/config.example.yaml diff --git a/config/integration/config.yaml b/configs/integration/config.yaml similarity index 100% rename from config/integration/config.yaml rename to configs/integration/config.yaml diff --git a/config/local/config.yaml b/configs/local/config.yaml similarity index 100% rename from config/local/config.yaml rename to configs/local/config.yaml diff --git a/config/prod/config.yaml b/configs/prod/config.yaml similarity index 100% rename from config/prod/config.yaml rename to configs/prod/config.yaml diff --git a/config/test/config.yaml b/configs/test/config.yaml similarity index 100% rename from config/test/config.yaml rename to configs/test/config.yaml diff --git a/internal/pkg/confpath/confpath.go b/internal/pkg/confpath/confpath.go index 78e5002..5d4f891 100644 --- a/internal/pkg/confpath/confpath.go +++ b/internal/pkg/confpath/confpath.go @@ -3,26 +3,41 @@ package confpath import ( "os" "path/filepath" + "runtime" "strings" ) // Resolve picks the config directory (Gin -e local|prod parity via -conf or -e). // Always returns an absolute path — safe for kratos run which changes working directory. func Resolve(confDir, env string) string { + // Normalize legacy "config" → "configs" + if confDir != "" { + confDir = normalizeConfigDirName(confDir) + } + if env != "" { // If confDir was provided, join env with it if confDir != "" { return makeAbsolute(filepath.Join(confDir, env)) } - // Default: config/{env} relative to project root - return makeAbsolute(filepath.Join("config", env)) + // Default: configs/{env} relative to project root + return makeAbsolute(filepath.Join("configs", env)) } if confDir == "" { - return makeAbsolute("./config") + return makeAbsolute("./configs") } return makeAbsolute(confDir) } +// normalizeConfigDirName replaces legacy "config" with "configs". +func normalizeConfigDirName(dir string) string { + base := filepath.Base(dir) + if strings.EqualFold(base, "config") { + return filepath.Join(filepath.Dir(dir), "configs") + } + return dir +} + // makeAbsolute converts a relative path to absolute by finding the project root (go.mod). func makeAbsolute(path string) string { if filepath.IsAbs(path) { @@ -43,6 +58,14 @@ func makeAbsolute(path string) string { return candidate } } + // Try from the source file location (this package is in internal/pkg/confpath) + srcRoot := findProjectRootFromSource() + if srcRoot != "" { + candidate := filepath.Join(srcRoot, path) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } // Last resort: just make it absolute from cwd abs, _ := filepath.Abs(path) return abs @@ -54,6 +77,11 @@ func findProjectRoot() string { if err != nil { return "" } + return searchUpForGoMod(dir) +} + +// searchUpForGoMod walks up the directory tree looking for go.mod. +func searchUpForGoMod(dir string) string { for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir @@ -66,3 +94,22 @@ func findProjectRoot() string { } return "" } + +// findProjectRootFromSource uses the location of this source file to find project root. +// This works even when the working directory is completely different. +func findProjectRootFromSource() string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + return "" + } + // This file is at internal/pkg/confpath/confpath.go + // Go up 4 levels to project root + dir := filepath.Dir(filename) // internal/pkg/confpath + dir = filepath.Dir(dir) // internal/pkg + dir = filepath.Dir(dir) // internal + dir = filepath.Dir(dir) // project root + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + return "" +} diff --git a/internal/pkg/confpath/confpath_test.go b/internal/pkg/confpath/confpath_test.go index e07f1fb..baabcad 100644 --- a/internal/pkg/confpath/confpath_test.go +++ b/internal/pkg/confpath/confpath_test.go @@ -15,8 +15,8 @@ func TestResolve(t *testing.T) { t.Fatal("could not find project root") } - want := filepath.Join(projectRoot, "config", "local") - if got := Resolve("./config", "local"); got != want { + want := filepath.Join(projectRoot, "configs", "local") + if got := Resolve("./configs", "local"); got != want { t.Fatalf("got %q want %q", got, want) } @@ -26,10 +26,10 @@ func TestResolve(t *testing.T) { t.Fatalf("expected absolute path, got %q", got) } - // Empty inputs should return absolute config path - wantConfig := filepath.Join(projectRoot, "config") - if got := Resolve("", ""); got != wantConfig { - t.Fatalf("got %q want %q", got, wantConfig) + // Empty inputs should return absolute configs path + wantConfigs := filepath.Join(projectRoot, "configs") + if got := Resolve("", ""); got != wantConfigs { + t.Fatalf("got %q want %q", got, wantConfigs) } } @@ -40,7 +40,7 @@ func TestResolveEnv(t *testing.T) { } // Test env-based resolution - want := filepath.Join(projectRoot, "config", "prod") + want := filepath.Join(projectRoot, "configs", "prod") if got := Resolve("", "prod"); got != want { t.Fatalf("got %q want %q", got, want) } @@ -54,12 +54,12 @@ func TestMakeAbsolute(t *testing.T) { } // Relative paths should become absolute - got := makeAbsolute("./config") + got := makeAbsolute("./configs") if !filepath.IsAbs(got) { t.Fatalf("expected absolute path, got %q", got) } - if !strings.HasSuffix(got, "config") { - t.Fatalf("expected path to end with 'config', got %q", got) + if !strings.HasSuffix(got, "configs") { + t.Fatalf("expected path to end with 'configs', got %q", got) } } diff --git a/output/outputGetAllTagsData.dat b/output/outputGetAllTagsData.dat new file mode 100644 index 0000000..84bd0c4 --- /dev/null +++ b/output/outputGetAllTagsData.dat @@ -0,0 +1 @@ +complete GetAllTagsData at 2026-06-04T23:53:26+08:00 From 8600e541748c4ebfbad3df0352c7b99a3c9abcf4 Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Fri, 5 Jun 2026 00:03:31 +0800 Subject: [PATCH 6/8] docs: update README with multi-environment config guide --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d965261..4127811 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,29 @@ docker run -p 8000:8000 -p 9000:9000 kratos-develop-template:latest ## 配置说明 -编辑 `config/config.yaml`: +### 环境配置 + +项目支持多环境配置,通过 `-e` 或 `--env` 指定: + +| 环境 | 配置目录 | 说明 | +|------|----------|------| +| `local` | `config/local/` | 本地开发(默认) | +| `bvt` | `config/bvt/` | 构建验证测试 | +| `test` | `config/test/` | 测试环境 | +| `prod` | `config/prod/` | 生产环境 | +| `integration` | `config/integration/` | 集成测试(kafka/rabbitmq enabled) | + +```bash +# 加载指定环境配置 +./bin/server -e local +./bin/server -e prod +./bin/task -e local savePageData +./bin/task -c /path/to/config -e test savePageDataCron --daemon +``` + +### 配置项示例 + +编辑 `config/config.yaml` 或对应环境的配置: ```yaml server: From 148b3cf5de7add12e2cc478b4f7179fbfb4dcdd8 Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Fri, 5 Jun 2026 00:04:35 +0800 Subject: [PATCH 7/8] docs: update README with kratos run examples --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4127811..1481e13 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,11 @@ go build -o bin/server ./cmd/server/ # Linux / macOS make generate && make wire && make build -# 运行(默认 MySQL/Redis/Kafka/RabbitMQ 均为 enabled:false,无需依赖即可启动) +# 使用 kratos 工具运行(推荐,自动处理工作目录和热重载) +kratos run -- -conf ./config -e local +kratos run -- -conf ./config -e prod + +# 直接运行二进制 ./bin/server -conf=./config # 指定环境配置(-e 或 --env,加载 config/{env}/config.yaml) @@ -112,6 +116,10 @@ go build -o bin/task ./cmd/task/ ./bin/task savePageDataCron # 爬数据入库(单次运行) ./bin/task savePageDataCron --daemon # 按 cron 循环运行 ./bin/task savePageDataCron -e prod --daemon # 生产环境循环爬取 + +# 使用 kratos run 运行 task +kratos run -- -conf ./config -e local savePageData +kratos run -- -conf ./config -e prod savePageDataCron --daemon ``` 默认 HTTP 端口 `8989`(与原 Gin 项目一致),Hello 接口:`GET http://127.0.0.1:8989/test` From e4d856ec158e92cc8b592f055fa7a6d16df0ea32 Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Fri, 5 Jun 2026 00:07:43 +0800 Subject: [PATCH 8/8] docs: update config paths from config/ to configs/ in README --- README.md | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1481e13..8d90af7 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,12 @@ kratos-develop-template/ │ ├── middleware/ # 中间件 │ └── conf/ # 配置定义 (protobuf 生成) │ └── conf.proto # 配置结构体定义 -├── config/ -│ └── config.yaml # 运行时配置 +├── configs/ # 多环境配置 +│ ├── config.yaml # 运行时配置(不提交,可能含密钥) +│ ├── config.example.yaml # 示例配置 +│ ├── local/ # 本地开发 +│ ├── prod/ # 生产环境 +│ └── integration/ # 集成测试 ├── third_party/ # 第三方 proto 依赖 ├── go.mod ├── go.sum @@ -95,13 +99,13 @@ go build -o bin/server ./cmd/server/ make generate && make wire && make build # 使用 kratos 工具运行(推荐,自动处理工作目录和热重载) -kratos run -- -conf ./config -e local -kratos run -- -conf ./config -e prod +kratos run -- -conf ./configs -e local +kratos run -- -conf ./configs -e prod # 直接运行二进制 -./bin/server -conf=./config +./bin/server -conf=./configs -# 指定环境配置(-e 或 --env,加载 config/{env}/config.yaml) +# 指定环境配置(-e 或 --env,加载 configs/{env}/config.yaml) ./bin/server -e local ./bin/server -e prod @@ -118,8 +122,8 @@ go build -o bin/task ./cmd/task/ ./bin/task savePageDataCron -e prod --daemon # 生产环境循环爬取 # 使用 kratos run 运行 task -kratos run -- -conf ./config -e local savePageData -kratos run -- -conf ./config -e prod savePageDataCron --daemon +kratos run -- -conf ./configs -e local savePageData +kratos run -- -conf ./configs -e prod savePageDataCron --daemon ``` 默认 HTTP 端口 `8989`(与原 Gin 项目一致),Hello 接口:`GET http://127.0.0.1:8989/test` @@ -128,7 +132,7 @@ kratos run -- -conf ./config -e prod savePageDataCron --daemon ```bash docker compose up -d -cp config/config.example.yaml config/config.yaml # 按需修改 enabled: true +cp configs/config.example.yaml configs/config.yaml # 按需修改 enabled: true ``` ### 6. OpenAPI @@ -160,7 +164,7 @@ $env:INTEGRATION_LEAVE_UP = "1" powershell -File scripts/integration-test.ps1 ``` -集成测试使用独立配置目录 `config/integration/`(`kafka` / `rabbitmq` 均为 `enabled: true`),构建标签为 `integration`,日常 `go test ./...` 不会执行。 +集成测试使用独立配置目录 `configs/integration/`(`kafka` / `rabbitmq` 均为 `enabled: true`),构建标签为 `integration`,日常 `go test ./...` 不会执行。 ### 8. 部署 @@ -184,23 +188,23 @@ docker run -p 8000:8000 -p 9000:9000 kratos-develop-template:latest | 环境 | 配置目录 | 说明 | |------|----------|------| -| `local` | `config/local/` | 本地开发(默认) | -| `bvt` | `config/bvt/` | 构建验证测试 | -| `test` | `config/test/` | 测试环境 | -| `prod` | `config/prod/` | 生产环境 | -| `integration` | `config/integration/` | 集成测试(kafka/rabbitmq enabled) | +| `local` | `configs/local/` | 本地开发(默认) | +| `bvt` | `configs/bvt/` | 构建验证测试 | +| `test` | `configs/test/` | 测试环境 | +| `prod` | `configs/prod/` | 生产环境 | +| `integration` | `configs/integration/` | 集成测试(kafka/rabbitmq enabled) | ```bash # 加载指定环境配置 ./bin/server -e local ./bin/server -e prod ./bin/task -e local savePageData -./bin/task -c /path/to/config -e test savePageDataCron --daemon +./bin/task -c ./configs -e test savePageDataCron --daemon ``` ### 配置项示例 -编辑 `config/config.yaml` 或对应环境的配置: +编辑 `configs/config.yaml` 或对应环境的配置: ```yaml server: