feat: 建立商品目录接入基础 (#132)
This commit is contained in:
+69
-2
@@ -31,6 +31,8 @@ const (
|
||||
databasePasswordEnv = "CMAUTOBUY_DB_PASSWORD"
|
||||
databaseTLSModeEnv = "CMAUTOBUY_DB_TLS_MODE"
|
||||
databaseTLSCAEnv = "CMAUTOBUY_DB_TLS_CA"
|
||||
catalogSourceEnv = "CMAUTOBUY_CATALOG_SOURCE"
|
||||
catalogTokenEnv = "CMAUTOBUY_CATALOG_TOKEN"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -52,6 +54,24 @@ type DatabaseConfig struct {
|
||||
TLSCA string `yaml:"tls_ca"`
|
||||
}
|
||||
|
||||
// CatalogIntegrationConfig 是第三方商品目录接口的独立身份配置。
|
||||
// Token 只允许来自未提交的 config.yaml 或环境变量,String 永远不输出明文。
|
||||
type CatalogIntegrationConfig struct {
|
||||
Source string `yaml:"source"`
|
||||
Token string `yaml:"token"`
|
||||
}
|
||||
|
||||
func (c CatalogIntegrationConfig) String() string {
|
||||
token := "(未启用)"
|
||||
if c.Token != "" {
|
||||
token = "****"
|
||||
}
|
||||
return fmt.Sprintf("CatalogIntegrationConfig{Source:%s Token:%s}", c.Source, token)
|
||||
}
|
||||
|
||||
// Enabled 表示商品目录接口已经配置专用凭据。
|
||||
func (c CatalogIntegrationConfig) Enabled() bool { return c.Token != "" }
|
||||
|
||||
// String 永远隐藏密码,防止排错时用 %v 把凭据写进日志。
|
||||
func (c DatabaseConfig) String() string {
|
||||
password := "(空)"
|
||||
@@ -98,6 +118,47 @@ func LoadDatabaseFromEnv() (DatabaseConfig, error) {
|
||||
return mergeDatabaseConfig(DatabaseConfig{})
|
||||
}
|
||||
|
||||
// LoadCatalogIntegration 读取可选的商品目录接口配置。未配置 Token 时接口保持禁用,
|
||||
// 不影响 Admin 其余页面和 Client 四接口启动。
|
||||
func LoadCatalogIntegration() (CatalogIntegrationConfig, error) {
|
||||
path, err := ConfigPath()
|
||||
if err != nil {
|
||||
return CatalogIntegrationConfig{}, fmt.Errorf("无法确定 config.yaml 应该在的位置: %w", err)
|
||||
}
|
||||
var cfg CatalogIntegrationConfig
|
||||
raw, readErr := os.ReadFile(path)
|
||||
if readErr == nil {
|
||||
parsed, parseErr := parseConfig(raw, path)
|
||||
if parseErr != nil {
|
||||
return CatalogIntegrationConfig{}, parseErr
|
||||
}
|
||||
cfg = parsed.Integrations.Catalog
|
||||
} else if !os.IsNotExist(readErr) {
|
||||
return CatalogIntegrationConfig{}, fmt.Errorf("读取配置文件 %s 失败: %w", path, readErr)
|
||||
}
|
||||
return mergeCatalogIntegration(cfg)
|
||||
}
|
||||
|
||||
func mergeCatalogIntegration(cfg CatalogIntegrationConfig) (CatalogIntegrationConfig, error) {
|
||||
if value := strings.TrimSpace(os.Getenv(catalogSourceEnv)); value != "" {
|
||||
cfg.Source = value
|
||||
}
|
||||
if value := os.Getenv(catalogTokenEnv); value != "" {
|
||||
cfg.Token = value
|
||||
}
|
||||
cfg.Source = strings.TrimSpace(cfg.Source)
|
||||
if cfg.Source == "" {
|
||||
cfg.Source = "external"
|
||||
}
|
||||
if len(cfg.Source) > 64 {
|
||||
return CatalogIntegrationConfig{}, fmt.Errorf("商品目录来源名称不能超过 64 个字符")
|
||||
}
|
||||
if cfg.Token != "" && len(cfg.Token) < 32 {
|
||||
return CatalogIntegrationConfig{}, fmt.Errorf("%s 至少需要 32 个字符", catalogTokenEnv)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// mergeDatabaseConfig 把环境变量合并到文件配置上。环境变量只要非空,
|
||||
// 就覆盖对应 YAML 字段,避免线上误读部署目录里遗留的本地配置。
|
||||
func mergeDatabaseConfig(cfg DatabaseConfig) (DatabaseConfig, error) {
|
||||
@@ -258,8 +319,14 @@ func (c SybConfig) String() string {
|
||||
|
||||
// Config 是 config.yaml 的顶层结构。
|
||||
type Config struct {
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Syb SybConfig `yaml:"syb"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Syb SybConfig `yaml:"syb"`
|
||||
Integrations IntegrationsConfig `yaml:"integrations"`
|
||||
}
|
||||
|
||||
// IntegrationsConfig 收拢所有非 Client 的第三方数据接入配置。
|
||||
type IntegrationsConfig struct {
|
||||
Catalog CatalogIntegrationConfig `yaml:"catalog"`
|
||||
}
|
||||
|
||||
// configFileName 是 config.yaml 相对 exe(或 go run 时相对工作目录)的文件名。
|
||||
|
||||
@@ -301,3 +301,29 @@ func TestLoad_配置文件损坏时报明确错误(t *testing.T) {
|
||||
t.Fatal("格式错误的 YAML 应该返回错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogIntegrationConfig_环境覆盖且不泄露Token(t *testing.T) {
|
||||
t.Setenv(catalogSourceEnv, "script-a")
|
||||
t.Setenv(catalogTokenEnv, "0123456789abcdef0123456789abcdef")
|
||||
cfg, err := mergeCatalogIntegration(CatalogIntegrationConfig{Source: "yaml", Token: "yaml-token-that-is-long-enough-123456"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Source != "script-a" || cfg.Token != "0123456789abcdef0123456789abcdef" {
|
||||
t.Fatalf("环境变量未覆盖:%s", cfg)
|
||||
}
|
||||
if strings.Contains(cfg.String(), cfg.Token) || !strings.Contains(cfg.String(), "****") {
|
||||
t.Fatalf("配置字符串泄露 Token:%s", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogIntegrationConfig_弱Token拒绝且留空可禁用(t *testing.T) {
|
||||
t.Setenv(catalogSourceEnv, "")
|
||||
t.Setenv(catalogTokenEnv, "")
|
||||
if cfg, err := mergeCatalogIntegration(CatalogIntegrationConfig{}); err != nil || cfg.Enabled() {
|
||||
t.Fatalf("留空应安全禁用:cfg=%s err=%v", cfg, err)
|
||||
}
|
||||
if _, err := mergeCatalogIntegration(CatalogIntegrationConfig{Token: "too-short"}); err == nil || !strings.Contains(err.Error(), catalogTokenEnv) {
|
||||
t.Fatalf("弱 Token 应被拒绝:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user