配置管理与Go服务架构


02 - 配置管理与 Go 服务架构


本章目标

  • 设计一个可持久化的配置系统
  • 用 Go struct + JSON 管理 API Key
  • 实现 API Key 的脱敏策略
  • 理解 Wails3 的 Service 注册机制
  • 将 ConfigService 注册到 Wails 应用

2.1 配置设计

我们的应用需要三个配置项:

配置项 用途 敏感度
deepseek_api_key DeepSeek 大模型 API Key 🔴 敏感
deepseek_model 模型名称(如 deepseek-chat 🟢 公开
pexels_api_key Pexels 图片搜索 API Key 🔴 敏感

配置文件存储在用户目录下的隐藏文件夹:

Windows: C:\Users\<用户名>\.pet-content-creator\config.json
macOS:   /Users/<用户名>/.pet-content-creator/config.json
Linux:   /home/<用户名>/.pet-content-creator/config.json

2.2 Go Struct 定义

创建 backend/config.go

package backend

// Config 应用配置
type Config struct {
    DeepSeekAPIKey string `json:"deepseek_api_key"`
    DeepSeekModel  string `json:"deepseek_model"`
    PexelsAPIKey   string `json:"pexels_api_key"`
}

💡 知识点: json:"deepseek_api_key" 是 Go 的 struct tag。encoding/json 包会读取这些 tag 来决定 JSON 字段名。如果不写 tag,Go 默认使用大写开头的字段名。


2.3 创建 ConfigService

继续在 backend/config.go 中添加:

import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
)

// ConfigService 配置管理服务(暴露给前端)
type ConfigService struct {
    configPath string
}

// NewConfigService 创建配置服务
func NewConfigService() *ConfigService {
    homeDir, _ := os.UserHomeDir()
    configDir := filepath.Join(homeDir, ".pet-content-creator")
    os.MkdirAll(configDir, 0700)
    return &ConfigService{
        configPath: filepath.Join(configDir, "config.json"),
    }
}

逐行解析

代码 说明
os.UserHomeDir() 获取当前用户的 home 目录
filepath.Join(...) 跨平台路径拼接(Windows 用 \,Unix 用 /
os.MkdirAll(..., 0700) 递归创建目录,权限为仅所有者可读写执行
configPath (小写) 小写字段 = 包外不可见,前端不会暴露这个字段

📌 关键概念: Wails3 只暴露 Service 的公开方法(大写开头的方法)给前端。字段(无论大小写)都不会直接暴露。这意味着 configPath 字段对前端是完全隐藏的。


2.4 JSON 配置读写

Load(加载配置)

// Load 加载配置(供后端内部调用)
func (cs *ConfigService) Load() (*Config, error) {
    data, err := os.ReadFile(cs.configPath)
    if err != nil {
        if os.IsNotExist(err) {
            // 文件不存在时返回默认配置
            return &Config{
                DeepSeekModel: "deepseek-chat",
            }, nil
        }
        return nil, fmt.Errorf("读取配置文件失败: %w", err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("解析配置文件失败: %w", err)
    }

    if cfg.DeepSeekModel == "" {
        cfg.DeepSeekModel = "deepseek-chat"
    }
    return &cfg, nil
}

💡 知识点: %w 格式化动词用于包装错误,保留原始错误链。调用方可以用 errors.Is() / errors.Unwrap() 检查原始错误类型。

Save(保存配置)

// Save 保存配置(供后端内部调用)
func (cs *ConfigService) Save(cfg *Config) error {
    data, err := json.MarshalIndent(cfg, "", "  ")
    if err != nil {
        return fmt.Errorf("序列化配置失败: %w", err)
    }
    if err := os.WriteFile(cs.configPath, data, 0600); err != nil {
        return fmt.Errorf("写入配置文件失败: %w", err)
    }
    return nil
}

💡 知识点: 文件权限 0600 = 仅所有者可读写。对于存储 API Key 的配置文件,这是基本安全习惯。


2.5 API Key 脱敏

配置保存在磁盘上是完整明文,但返回给前端时必须脱敏。否则任何人打开浏览器控制台就能看到你的 API Key。

脱敏策略

sk-abcdefghijklmnopqrstuvwxyz123456
→   sk-ab**********************3456

只保留前 4 位和后 4 位,中间替换为 ****

// GetConfig 获取配置(前端调用)
func (cs *ConfigService) GetConfig() (*Config, error) {
    cfg, err := cs.Load()
    if err != nil {
        return nil, err
    }
    // 返回脱敏后的 API Key
    if len(cfg.DeepSeekAPIKey) > 8 {
        cfg.DeepSeekAPIKey = cfg.DeepSeekAPIKey[:4] + "****" + 
            cfg.DeepSeekAPIKey[len(cfg.DeepSeekAPIKey)-4:]
    }
    if len(cfg.PexelsAPIKey) > 8 {
        cfg.PexelsAPIKey = cfg.PexelsAPIKey[:4] + "****" + 
            cfg.PexelsAPIKey[len(cfg.PexelsAPIKey)-4:]
    }
    return cfg, nil
}

保存时处理脱敏值

用户在前端修改配置时,脱敏字段可能还是 sk-ab****3456。我们需要判断:如果传入的是脱敏值,保留磁盘上的原有 API Key:

// SaveConfig 保存配置(前端调用,处理脱敏值)
func (cs *ConfigService) SaveConfig(cfg *Config) error {
    // 如果传入的是脱敏值,保留原有的 API Key
    if len(cfg.DeepSeekAPIKey) > 4 && cfg.DeepSeekAPIKey[4:8] == "****" {
        existing, err := cs.Load()
        if err == nil && existing != nil {
            cfg.DeepSeekAPIKey = existing.DeepSeekAPIKey
        }
    }
    if len(cfg.PexelsAPIKey) > 4 && cfg.PexelsAPIKey[4:8] == "****" {
        existing, err := cs.Load()
        if err == nil && existing != nil {
            cfg.PexelsAPIKey = existing.PexelsAPIKey
        }
    }
    return cs.Save(cfg)
}

⚠️ 安全提示: 这种脱敏策略适合桌面应用(单用户本地运行)。如果是 Web 服务,还需要额外考虑传输层加密(HTTPS)和内存安全。


2.6 服务注册到 Wails

回到 main.go,将 ConfigService 注册为 Wails Service:

import (
    "pet-content-creator/backend"
)

func main() {
    // 创建配置服务(共享)
    configService := backend.NewConfigService()

    // 创建 Wails 应用
    app := application.New(application.Options{
        Name:        "pet-content-creator",
        Description: "AI驱动的今日头条宠物内容创作工具",
        Services: []application.Service{
            application.NewService(configService),
        },
        Assets: application.AssetOptions{
            Handler: application.AssetFileServerFS(assets),
        },
        Mac: application.MacOptions{
            ApplicationShouldTerminateAfterLastWindowClosed: true,
        },
    })

    // ... 其余代码不变
}

编译验证

wails3 dev

此时 Wails3 会自动:

  1. 扫描 ConfigService 的公开方法
  2. frontend/bindings/ 下生成 TypeScript 绑定文件
  3. 前端可以 import { ConfigService } from "../bindings/..." 直接调用

生成的文件大概是这样的(不需要手写):

// frontend/bindings/pet-content-creator/backend/configservice.ts
export class ConfigService {
    static GetConfig(): Promise<Config | null>;
    static SaveConfig(cfg: Config): Promise<void>;
}

2.7 前端调用配置服务

修改 frontend/src/App.tsx,添加配置面板:

import { useState, useEffect } from "react";
import { ConfigService } from "../bindings/pet-content-creator/backend";

interface Config {
  deepseek_api_key: string;
  deepseek_model: string;
  pexels_api_key: string;
}

function App() {
  const [showConfig, setShowConfig] = useState(false);
  const [config, setConfig] = useState<Config>({
    deepseek_api_key: "",
    deepseek_model: "deepseek-chat",
    pexels_api_key: "",
  });

  // 页面加载时读取配置
  useEffect(() => {
    ConfigService.GetConfig()
      .then((cfg) => {
        if (cfg) setConfig(cfg);
      })
      .catch(() => {});
  }, []);

  // 保存配置
  const saveConfig = async () => {
    try {
      await ConfigService.SaveConfig(config);
      alert("配置已保存");
      setShowConfig(false);
    } catch (e: any) {
      alert("保存失败: " + e.message);
    }
  };

  return (
    <div className="app">
      <header>
        <h1>🐾 今日头条宠物内容创作工具</h1>
        <button onClick={() => setShowConfig(!showConfig)}>
          ⚙️ 配置
        </button>
      </header>

      {showConfig && (
        <div className="config-panel">
          <h3>⚙️ API 配置</h3>

          <label>DeepSeek API Key</label>
          <input
            type="password"
            value={config.deepseek_api_key}
            onChange={(e) => setConfig({ ...config, deepseek_api_key: e.target.value })}
            placeholder="sk-..."
          />

          <label>DeepSeek 模型</label>
          <input
            type="text"
            value={config.deepseek_model}
            onChange={(e) => setConfig({ ...config, deepseek_model: e.target.value })}
          />

          <label>Pexels API Key</label>
          <input
            type="password"
            value={config.pexels_api_key}
            onChange={(e) => setConfig({ ...config, pexels_api_key: e.target.value })}
            placeholder="Pexels API Key"
          />

          <button onClick={saveConfig}>💾 保存配置</button>
        </div>
      )}
    </div>
  );
}

export default App;

⚠️ 注意: input type="password" 会让浏览器将输入框显示为密码模式(圆点遮挡),但这只是视觉上的。实际传给后端的值仍然是明文。


2.8 服务架构模式

此时我们的后端架构如下:

┌─────────────────────────────────────┐
│           main.go (入口)             │
│                                     │
│  configService = NewConfigService() │
│        └── 注册为 Wails Service ──→ 前端可调用
│                                     │
│  其他 Service 也共享 configService  │
│  (后续章节中创建)                    │
└─────────────────────────────────────┘

📌 关键概念: ConfigService 是一个共享服务。它在 main.go 中创建一次,然后传递给所有其他服务(AgentService、PexelsService 等)。这是典型的依赖注入模式。


本章总结

你已经学会 对应能力
Go struct + JSON tag 定义数据结构
os.ReadFile / os.WriteFile 文件持久化
json.MarshalIndent / json.Unmarshal JSON 序列化
API Key 脱敏策略 安全最佳实践
application.NewService(...) Wails3 服务注册
React useState + useEffect 前端状态管理

🔧 动手练习

  1. 新增一个配置项 output_dir(输出目录路径),允许用户自定义 Word 文档保存位置
  2. 修改脱敏逻辑,让长度 ≤ 8 的 API Key 也能部分脱敏
  3. config.json 中手动写入错误格式的 JSON,启动应用观察错误处理是否生效
  4. 尝试在前端增加一个"重置配置"按钮,调用后清除所有配置项

👉 下一章:AI 文章生成 — Eino 集成 DeepSeek

wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo