|
|
# 管理后台开发指南
|
|
|
|
|
|
本文档适用于 **Go** 和 **Python** 双实现。示例代码会标注对应的实现语言。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 快速开始
|
|
|
|
|
|
### 1. 环境准备
|
|
|
|
|
|
#### Go 环境
|
|
|
|
|
|
```bash
|
|
|
# 确保Go版本 >= 1.21
|
|
|
go version
|
|
|
|
|
|
# 安装依赖
|
|
|
cd market-data-service
|
|
|
go mod download
|
|
|
```
|
|
|
|
|
|
#### Python 环境
|
|
|
|
|
|
```bash
|
|
|
# 确保Python版本 >= 3.10
|
|
|
python --version
|
|
|
|
|
|
# 创建虚拟环境
|
|
|
cd python_market_data_service
|
|
|
python -m venv venv
|
|
|
|
|
|
# 激活虚拟环境
|
|
|
# Linux/Mac:
|
|
|
source venv/bin/activate
|
|
|
# Windows:
|
|
|
venv\Scripts\activate
|
|
|
|
|
|
# 安装依赖
|
|
|
pip install -r requirements.txt
|
|
|
pip install tushare
|
|
|
```
|
|
|
|
|
|
### 2. 启动服务
|
|
|
|
|
|
#### Go
|
|
|
|
|
|
```bash
|
|
|
# 方式1:直接运行
|
|
|
go run ./cmd/server
|
|
|
|
|
|
# 方式2:使用配置文件
|
|
|
export CONFIG_PATH="./config.json"
|
|
|
go run ./cmd/server
|
|
|
|
|
|
# 方式3:Makefile
|
|
|
make run
|
|
|
```
|
|
|
|
|
|
#### Python
|
|
|
|
|
|
```bash
|
|
|
# 方式1:直接运行(开发模式)
|
|
|
python -m app.main
|
|
|
|
|
|
# 方式2:使用uvicorn(推荐)
|
|
|
uvicorn app.main:app --reload --port 8080
|
|
|
|
|
|
# 方式3:生产模式
|
|
|
uvicorn app.main:app --host 0.0.0.0 --port 8080 --workers 4
|
|
|
```
|
|
|
|
|
|
### 3. 访问管理后台
|
|
|
|
|
|
浏览器打开:`http://localhost:8080/admin`
|
|
|
|
|
|
API文档:`http://localhost:8080/docs` (仅Python/FastAPI自动生成)
|
|
|
|
|
|
---
|
|
|
|
|
|
## 开发新功能
|
|
|
|
|
|
### 场景1:添加新的配置项
|
|
|
|
|
|
假设需要添加一个新的缓存配置项:
|
|
|
|
|
|
#### Go 实现
|
|
|
|
|
|
**步骤1**: 修改配置结构 (`pkg/config/config.go`)
|
|
|
|
|
|
```go
|
|
|
type Config struct {
|
|
|
// ... 现有配置
|
|
|
Cache CacheConfig `json:"cache"`
|
|
|
}
|
|
|
|
|
|
type CacheConfig struct {
|
|
|
Enabled bool `json:"enabled"`
|
|
|
TTL int `json:"ttl"` // 缓存过期时间(秒)
|
|
|
MaxSize int `json:"max_size"` // 最大缓存条目数
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**步骤2**: 在配置服务中添加新分组 (`internal/service/config.go`)
|
|
|
|
|
|
```go
|
|
|
func (s *ConfigServiceImpl) GetConfigList(...) {
|
|
|
// ... 现有配置分组
|
|
|
|
|
|
// 添加缓存配置分组
|
|
|
sections = append(sections, api.ConfigSection{
|
|
|
Name: "缓存配置",
|
|
|
Type: api.ConfigType("cache"), // 添加新的类型常量
|
|
|
Description: "数据缓存相关配置",
|
|
|
Items: []api.ConfigItem{
|
|
|
{
|
|
|
Key: "enabled",
|
|
|
Value: s.config.Cache.Enabled,
|
|
|
Type: "bool",
|
|
|
Description: "是否启用缓存",
|
|
|
Editable: true,
|
|
|
Required: false,
|
|
|
},
|
|
|
{
|
|
|
Key: "ttl",
|
|
|
Value: s.config.Cache.TTL,
|
|
|
Type: "int",
|
|
|
Description: "缓存过期时间(秒)",
|
|
|
Editable: true,
|
|
|
Required: false,
|
|
|
},
|
|
|
{
|
|
|
Key: "max_size",
|
|
|
Value: s.config.Cache.MaxSize,
|
|
|
Type: "int",
|
|
|
Description: "最大缓存条目数",
|
|
|
Editable: true,
|
|
|
Required: false,
|
|
|
},
|
|
|
},
|
|
|
})
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**步骤3**: 添加配置更新处理
|
|
|
|
|
|
```go
|
|
|
func (s *ConfigServiceImpl) UpdateConfig(...) {
|
|
|
switch req.Type {
|
|
|
// ... 现有 case
|
|
|
|
|
|
case api.ConfigType("cache"):
|
|
|
if enabled, ok := req.Items["enabled"]; ok {
|
|
|
s.config.Cache.Enabled = enabled.(bool)
|
|
|
}
|
|
|
if ttl, ok := req.Items["ttl"]; ok {
|
|
|
s.config.Cache.TTL = int(ttl.(float64))
|
|
|
}
|
|
|
if maxSize, ok := req.Items["max_size"]; ok {
|
|
|
s.config.Cache.MaxSize = int(maxSize.(float64))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 保存配置
|
|
|
if err := s.saveConfig(); err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
return &api.ConfigUpdateData{
|
|
|
Success: true,
|
|
|
NeedRestart: false, // 缓存配置支持热加载
|
|
|
Message: "配置更新成功",
|
|
|
}, nil
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**步骤4**: 添加配置变更回调(可选)
|
|
|
|
|
|
```go
|
|
|
// 在初始化时注册回调
|
|
|
configService.RegisterCallback(api.ConfigType("cache"), func() {
|
|
|
// 重新初始化缓存
|
|
|
cache.Init(configService.GetCurrentConfig().Cache)
|
|
|
})
|
|
|
```
|
|
|
|
|
|
#### Python 实现
|
|
|
|
|
|
**步骤1**: 修改配置结构 (`app/core/config.py`)
|
|
|
|
|
|
```python
|
|
|
class CacheConfig(BaseModel):
|
|
|
"""缓存配置"""
|
|
|
enabled: bool = False
|
|
|
ttl: int = 3600 # 缓存过期时间(秒)
|
|
|
max_size: int = 1000 # 最大缓存条目数
|
|
|
|
|
|
class Config(BaseModel):
|
|
|
"""主配置类"""
|
|
|
# ... 现有配置
|
|
|
cache: CacheConfig = Field(default_factory=CacheConfig)
|
|
|
```
|
|
|
|
|
|
**步骤2**: 在配置服务中添加新分组 (`app/services/config_service.py`)
|
|
|
|
|
|
```python
|
|
|
from app.models import ConfigType
|
|
|
|
|
|
class ConfigService:
|
|
|
def get_config_list(self, req: ConfigListRequest) -> ConfigListData:
|
|
|
sections = []
|
|
|
|
|
|
# ... 现有配置分组
|
|
|
|
|
|
# 添加缓存配置分组
|
|
|
if not req.type or req.type == ConfigType.CACHE:
|
|
|
sections.append(ConfigSection(
|
|
|
name="缓存配置",
|
|
|
type=ConfigType.CACHE,
|
|
|
description="数据缓存相关配置",
|
|
|
items=[
|
|
|
ConfigItem(
|
|
|
key="enabled",
|
|
|
value=self.config.cache.enabled,
|
|
|
type="bool",
|
|
|
description="是否启用缓存",
|
|
|
editable=True,
|
|
|
required=False
|
|
|
),
|
|
|
ConfigItem(
|
|
|
key="ttl",
|
|
|
value=self.config.cache.ttl,
|
|
|
type="int",
|
|
|
description="缓存过期时间(秒)",
|
|
|
editable=True,
|
|
|
required=False
|
|
|
),
|
|
|
ConfigItem(
|
|
|
key="max_size",
|
|
|
value=self.config.cache.max_size,
|
|
|
type="int",
|
|
|
description="最大缓存条目数",
|
|
|
editable=True,
|
|
|
required=False
|
|
|
),
|
|
|
]
|
|
|
))
|
|
|
|
|
|
return ConfigListData(sections=sections, ...)
|
|
|
```
|
|
|
|
|
|
**步骤3**: 添加配置更新处理
|
|
|
|
|
|
```python
|
|
|
def update_config(self, req: ConfigUpdateRequest) -> ConfigUpdateData:
|
|
|
need_restart = False
|
|
|
|
|
|
with self.lock:
|
|
|
if req.type == ConfigType.CACHE:
|
|
|
if "enabled" in req.items:
|
|
|
self.config.cache.enabled = bool(req.items["enabled"])
|
|
|
if "ttl" in req.items:
|
|
|
self.config.cache.ttl = int(req.items["ttl"])
|
|
|
if "max_size" in req.items:
|
|
|
self.config.cache.max_size = int(req.items["max_size"])
|
|
|
|
|
|
# 保存到文件
|
|
|
try:
|
|
|
save_config(self.config)
|
|
|
self._trigger_callbacks(req.type)
|
|
|
|
|
|
return ConfigUpdateData(
|
|
|
success=True,
|
|
|
need_restart=False, # 缓存配置支持热加载
|
|
|
message="配置更新成功"
|
|
|
)
|
|
|
except Exception as e:
|
|
|
return ConfigUpdateData(
|
|
|
success=False,
|
|
|
message=f"配置保存失败: {e}"
|
|
|
)
|
|
|
```
|
|
|
|
|
|
**步骤4**: 添加配置变更回调(可选)
|
|
|
|
|
|
```python
|
|
|
# 在初始化时注册回调
|
|
|
config_service.register_callback(ConfigType.CACHE, lambda:
|
|
|
cache.init(config_service.get_current_config().cache)
|
|
|
)
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
### 场景2:添加新的数据源适配器
|
|
|
|
|
|
假设需要添加一个名为 "mydata" 的适配器:
|
|
|
|
|
|
#### Go 实现
|
|
|
|
|
|
**步骤1**: 创建适配器实现 (`adapter/mydata/adapter.go`)
|
|
|
|
|
|
```go
|
|
|
package mydata
|
|
|
|
|
|
import (
|
|
|
"market-data-service/adapter"
|
|
|
)
|
|
|
|
|
|
type Adapter struct {
|
|
|
client *Client
|
|
|
config map[string]string
|
|
|
}
|
|
|
|
|
|
func NewAdapter() *Adapter {
|
|
|
return &Adapter{}
|
|
|
}
|
|
|
|
|
|
func (a *Adapter) Connect(config map[string]string) error {
|
|
|
a.config = config
|
|
|
// 初始化客户端连接
|
|
|
a.client = NewClient(config["api_key"])
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (a *Adapter) SubscribeTicks(symbols []string, callback adapter.TickCallback) error {
|
|
|
// 实现实时数据订阅
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (a *Adapter) FetchKLines(symbol, start, end, freq string) ([]adapter.KLineData, error) {
|
|
|
// 实现K线数据获取
|
|
|
return nil, nil
|
|
|
}
|
|
|
|
|
|
func (a *Adapter) FetchSymbols(assetType string) ([]adapter.SymbolInfo, error) {
|
|
|
// 实现标的列表获取
|
|
|
return nil, nil
|
|
|
}
|
|
|
|
|
|
func (a *Adapter) FetchTradingCalendar(exchange, start, end string) ([]adapter.TradeCalData, error) {
|
|
|
// 实现交易日历获取
|
|
|
return nil, nil
|
|
|
}
|
|
|
|
|
|
func (a *Adapter) HealthCheck() error {
|
|
|
// 实现健康检查
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (a *Adapter) Close() error {
|
|
|
// 实现资源释放
|
|
|
return nil
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**步骤2**: 注册适配器 (`internal/service/adapter.go`)
|
|
|
|
|
|
```go
|
|
|
import "market-data-service/adapter/mydata"
|
|
|
|
|
|
func (s *AdapterServiceImpl) registerBuiltinAdapters() {
|
|
|
// ... 现有适配器注册
|
|
|
|
|
|
// 注册 MyData 适配器
|
|
|
s.RegisterAdapter("mydata", func() adapter.DataSourceAdapter {
|
|
|
return mydata.NewAdapter()
|
|
|
})
|
|
|
|
|
|
// 设置元数据
|
|
|
s.metadata["mydata"] = &adapterMetadata{
|
|
|
Name: "mydata",
|
|
|
Type: "http",
|
|
|
Version: "1.0.0",
|
|
|
Description: "MyData 金融数据接口",
|
|
|
UpdatedAt: time.Now(),
|
|
|
}
|
|
|
|
|
|
// 默认配置
|
|
|
s.configs["mydata"] = &adapterConfig{
|
|
|
Enabled: false,
|
|
|
Config: map[string]string{
|
|
|
"api_key": "",
|
|
|
"base_url": "https://api.mydata.com",
|
|
|
},
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
#### Python 实现
|
|
|
|
|
|
**步骤1**: 创建适配器实现 (`app/adapters/mydata_adapter.py`)
|
|
|
|
|
|
```python
|
|
|
from typing import List
|
|
|
from app.adapters.base import (
|
|
|
DataSourceAdapter, TickData, KLineData,
|
|
|
SymbolInfo, TradeCalData, TickCallback
|
|
|
)
|
|
|
|
|
|
class MyDataAdapter(DataSourceAdapter):
|
|
|
"""MyData数据源适配器"""
|
|
|
|
|
|
def __init__(self):
|
|
|
self.client = None
|
|
|
self.config = {}
|
|
|
|
|
|
async def connect(self, config: dict) -> None:
|
|
|
self.config = config
|
|
|
# 初始化客户端连接
|
|
|
self.client = MyDataClient(config["api_key"])
|
|
|
|
|
|
async def subscribe_ticks(self, symbols: List[str], callback: TickCallback) -> None:
|
|
|
# 实现实时数据订阅
|
|
|
pass
|
|
|
|
|
|
async def fetch_klines(
|
|
|
self, symbol: str, start: str, end: str, freq: str
|
|
|
) -> List[KLineData]:
|
|
|
# 实现K线数据获取
|
|
|
return []
|
|
|
|
|
|
async def fetch_symbols(self, asset_type: str) -> List[SymbolInfo]:
|
|
|
# 实现标的列表获取
|
|
|
return []
|
|
|
|
|
|
async def fetch_trading_calendar(
|
|
|
self, exchange: str, start: str, end: str
|
|
|
) -> List[TradeCalData]:
|
|
|
# 实现交易日历获取
|
|
|
return []
|
|
|
|
|
|
async def health_check(self) -> bool:
|
|
|
# 实现健康检查
|
|
|
return True
|
|
|
|
|
|
async def close(self) -> None:
|
|
|
# 实现资源释放
|
|
|
pass
|
|
|
```
|
|
|
|
|
|
**步骤2**: 注册适配器 (`app/services/adapter_service.py`)
|
|
|
|
|
|
```python
|
|
|
from app.adapters.mydata_adapter import MyDataAdapter
|
|
|
|
|
|
class AdapterService:
|
|
|
def _register_builtin_adapters(self):
|
|
|
# ... 现有适配器注册
|
|
|
|
|
|
# 注册 MyData 适配器
|
|
|
self.register_adapter("mydata", lambda: MyDataAdapter())
|
|
|
|
|
|
# 设置元数据
|
|
|
self.metadata["mydata"] = {
|
|
|
"name": "mydata",
|
|
|
"type": "http",
|
|
|
"version": "1.0.0",
|
|
|
"description": "MyData 金融数据接口",
|
|
|
"updated_at": datetime.now()
|
|
|
}
|
|
|
|
|
|
# 默认配置
|
|
|
self.configs["mydata"] = {
|
|
|
"enabled": False,
|
|
|
"config": {
|
|
|
"api_key": "",
|
|
|
"base_url": "https://api.mydata.com"
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**步骤3**: 前端会自动从 `/v1/admin/adapters` 接口获取适配器列表,无需额外修改。
|
|
|
|
|
|
---
|
|
|
|
|
|
### 场景3:添加新的接口测试用例
|
|
|
|
|
|
#### Go 实现
|
|
|
|
|
|
**步骤1**: 在测试服务中添加用例 (`internal/service/test.go`)
|
|
|
|
|
|
```go
|
|
|
func (s *TestServiceImpl) GetAPITestList(...) {
|
|
|
categories := []api.APITestCategory{
|
|
|
// ... 现有分类
|
|
|
|
|
|
{
|
|
|
Name: "自定义接口",
|
|
|
Items: []api.APITestCase{
|
|
|
{
|
|
|
ID: "custom_endpoint",
|
|
|
Name: "自定义端点测试",
|
|
|
Method: "GET",
|
|
|
Path: "/v1/custom/{id}",
|
|
|
Description: "测试自定义端点",
|
|
|
Params: map[string]string{
|
|
|
"id": "123",
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
ID: "custom_post",
|
|
|
Name: "自定义POST接口",
|
|
|
Method: "POST",
|
|
|
Path: "/v1/custom/create",
|
|
|
Description: "测试创建接口",
|
|
|
Body: map[string]interface{}{
|
|
|
"name": "test",
|
|
|
"value": 100,
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
},
|
|
|
}
|
|
|
|
|
|
return &api.APITestListData{
|
|
|
Categories: categories,
|
|
|
BaseURL: "",
|
|
|
}, nil
|
|
|
}
|
|
|
```
|
|
|
|
|
|
#### Python 实现
|
|
|
|
|
|
**步骤1**: 在测试服务中添加用例 (`app/services/test_service.py`)
|
|
|
|
|
|
```python
|
|
|
def get_api_test_list(self) -> APITestListData:
|
|
|
categories = [
|
|
|
# ... 现有分类
|
|
|
|
|
|
APITestCategory(
|
|
|
name="自定义接口",
|
|
|
items=[
|
|
|
APITestCase(
|
|
|
id="custom_endpoint",
|
|
|
name="自定义端点测试",
|
|
|
method="GET",
|
|
|
path="/v1/custom/{id}",
|
|
|
description="测试自定义端点",
|
|
|
params={"id": "123"}
|
|
|
),
|
|
|
APITestCase(
|
|
|
id="custom_post",
|
|
|
name="自定义POST接口",
|
|
|
method="POST",
|
|
|
path="/v1/custom/create",
|
|
|
description="测试创建接口",
|
|
|
body={"name": "test", "value": 100}
|
|
|
),
|
|
|
]
|
|
|
),
|
|
|
]
|
|
|
|
|
|
return APITestListData(categories=categories, base_url="")
|
|
|
```
|
|
|
|
|
|
**步骤2**: 前端会自动显示新的测试用例,无需修改前端代码。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 调试技巧
|
|
|
|
|
|
### 1. 查看配置变更日志
|
|
|
|
|
|
#### Go
|
|
|
|
|
|
```go
|
|
|
// 在 ConfigService 中添加日志
|
|
|
func (s *ConfigServiceImpl) UpdateConfig(...) {
|
|
|
log.Printf("[Config] Updating config type: %s", req.Type)
|
|
|
log.Printf("[Config] Update items: %+v", req.Items)
|
|
|
|
|
|
// ... 更新逻辑
|
|
|
|
|
|
log.Printf("[Config] Config updated successfully, need restart: %v", needRestart)
|
|
|
}
|
|
|
```
|
|
|
|
|
|
#### Python
|
|
|
|
|
|
```python
|
|
|
# 在 ConfigService 中添加日志
|
|
|
from app.core.logger import info
|
|
|
|
|
|
def update_config(self, req: ConfigUpdateRequest) -> ConfigUpdateData:
|
|
|
info(f"[Config] Updating config type: {req.type}")
|
|
|
info(f"[Config] Update items: {req.items}")
|
|
|
|
|
|
# ... 更新逻辑
|
|
|
|
|
|
info(f"[Config] Config updated successfully, need restart: {need_restart}")
|
|
|
```
|
|
|
|
|
|
### 2. 适配器调试
|
|
|
|
|
|
#### Go
|
|
|
|
|
|
```go
|
|
|
// 在适配器中添加详细日志
|
|
|
func (a *MyAdapter) FetchKLines(...) {
|
|
|
log.Printf("[MyAdapter] FetchKLines called: symbol=%s, start=%s, end=%s, freq=%s",
|
|
|
symbol, start, end, freq)
|
|
|
|
|
|
// ... 实现逻辑
|
|
|
|
|
|
log.Printf("[MyAdapter] FetchKLines completed: got %d records", len(result))
|
|
|
return result, nil
|
|
|
}
|
|
|
```
|
|
|
|
|
|
#### Python
|
|
|
|
|
|
```python
|
|
|
# 在适配器中添加详细日志
|
|
|
from app.core.logger import info
|
|
|
|
|
|
async def fetch_klines(self, symbol: str, start: str, end: str, freq: str) -> List[KLineData]:
|
|
|
info(f"[MyAdapter] FetchKLines called: symbol={symbol}, start={start}, end={end}, freq={freq}")
|
|
|
|
|
|
# ... 实现逻辑
|
|
|
|
|
|
info(f"[MyAdapter] FetchKLines completed: got {len(result)} records")
|
|
|
return result
|
|
|
```
|
|
|
|
|
|
### 3. 使用调试器
|
|
|
|
|
|
#### Go (Delve)
|
|
|
|
|
|
```bash
|
|
|
# 启动调试模式
|
|
|
dlv debug ./cmd/server
|
|
|
|
|
|
# 在关键位置设置断点
|
|
|
(dlv) break internal/service/config.go:100
|
|
|
(dlv) break internal/service/adapter.go:150
|
|
|
|
|
|
# 运行
|
|
|
(dlv) continue
|
|
|
```
|
|
|
|
|
|
#### Python (pdb 或 IDE)
|
|
|
|
|
|
```python
|
|
|
# 代码中插入断点
|
|
|
import pdb; pdb.set_trace()
|
|
|
|
|
|
# 或使用IPython
|
|
|
from IPython import embed; embed()
|
|
|
```
|
|
|
|
|
|
或使用 VS Code / PyCharm 的图形化调试。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 测试
|
|
|
|
|
|
### 单元测试示例
|
|
|
|
|
|
#### Go
|
|
|
|
|
|
```go
|
|
|
// internal/service/config_test.go
|
|
|
package service
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
"testing"
|
|
|
|
|
|
"market-data-service/api"
|
|
|
)
|
|
|
|
|
|
func TestConfigService_UpdateConfig(t *testing.T) {
|
|
|
// 创建临时配置文件
|
|
|
tmpFile := t.TempDir() + "/config.json"
|
|
|
|
|
|
service, err := NewConfigService(tmpFile)
|
|
|
if err != nil {
|
|
|
t.Fatal(err)
|
|
|
}
|
|
|
|
|
|
// 测试更新配置
|
|
|
req := &api.ConfigUpdateRequest{
|
|
|
Type: api.ConfigTypeServer,
|
|
|
Items: map[string]interface{}{
|
|
|
"port": 9090,
|
|
|
},
|
|
|
}
|
|
|
|
|
|
result, err := service.UpdateConfig(context.Background(), req)
|
|
|
if err != nil {
|
|
|
t.Fatal(err)
|
|
|
}
|
|
|
|
|
|
if !result.Success {
|
|
|
t.Errorf("expected success, got: %v", result.Message)
|
|
|
}
|
|
|
|
|
|
// 验证配置已更新
|
|
|
cfg := service.GetCurrentConfig()
|
|
|
if cfg.Server.Port != 9090 {
|
|
|
t.Errorf("expected port 9090, got: %d", cfg.Server.Port)
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
#### Python
|
|
|
|
|
|
```python
|
|
|
# tests/test_config_service.py
|
|
|
import pytest
|
|
|
from app.services.config_service import ConfigService
|
|
|
from app.models import ConfigUpdateRequest, ConfigType
|
|
|
|
|
|
def test_config_service_update():
|
|
|
# 创建服务
|
|
|
service = ConfigService()
|
|
|
|
|
|
# 测试更新配置
|
|
|
req = ConfigUpdateRequest(
|
|
|
type=ConfigType.SERVER,
|
|
|
items={"port": 9090}
|
|
|
)
|
|
|
|
|
|
result = service.update_config(req)
|
|
|
|
|
|
assert result.success is True
|
|
|
assert service.get_current_config().server.port == 9090
|
|
|
```
|
|
|
|
|
|
运行测试:
|
|
|
|
|
|
```bash
|
|
|
# Go
|
|
|
go test ./internal/service/...
|
|
|
|
|
|
# Python
|
|
|
pytest tests/
|
|
|
```
|
|
|
|
|
|
### API 测试
|
|
|
|
|
|
```bash
|
|
|
# 使用 httpie 测试
|
|
|
http GET localhost:8080/v1/admin/system/status
|
|
|
|
|
|
http POST localhost:8080/v1/admin/config \
|
|
|
type=server \
|
|
|
items:='{"port": 8080}'
|
|
|
|
|
|
# 使用 curl 测试
|
|
|
curl -X POST "http://localhost:8080/v1/admin/tests/api/run" \
|
|
|
-H "Content-Type: application/json" \
|
|
|
-d '{"id": "stock_klines", "params": {"symbol": "000001.SZ"}}'
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
## 常见问题
|
|
|
|
|
|
### Q1: 配置热加载不生效?
|
|
|
|
|
|
**检查点**:
|
|
|
1. 配置文件路径是否正确
|
|
|
2. 配置类型是否支持热加载(数据库配置需重启)
|
|
|
3. 回调函数是否正确注册
|
|
|
|
|
|
**Go**:
|
|
|
```go
|
|
|
// 检查配置是否正确保存
|
|
|
config, _ := service.GetConfigList(ctx, &api.ConfigListRequest{})
|
|
|
fmt.Printf("Current config: %+v\n", config)
|
|
|
```
|
|
|
|
|
|
**Python**:
|
|
|
```python
|
|
|
# 检查配置是否正确保存
|
|
|
config = service.get_config_list(ConfigListRequest())
|
|
|
print(f"Current config: {config}")
|
|
|
```
|
|
|
|
|
|
### Q2: 适配器无法启用?
|
|
|
|
|
|
**检查点**:
|
|
|
1. 适配器是否已注册
|
|
|
2. 配置项是否完整(如 token)
|
|
|
3. Connect 方法是否返回错误
|
|
|
|
|
|
**Go**:
|
|
|
```go
|
|
|
// 添加调试日志
|
|
|
func (s *AdapterServiceImpl) ToggleAdapter(...) {
|
|
|
log.Printf("[Adapter] Toggling adapter: %s, enable: %v", req.Name, req.Enable)
|
|
|
|
|
|
cfg, ok := s.configs[req.Name]
|
|
|
if !ok {
|
|
|
log.Printf("[Adapter] Adapter not found: %s", req.Name)
|
|
|
return fmt.Errorf("adapter not found: %s", req.Name)
|
|
|
}
|
|
|
|
|
|
log.Printf("[Adapter] Current config: %+v", cfg)
|
|
|
// ...
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**Python**:
|
|
|
```python
|
|
|
# 添加调试日志
|
|
|
def toggle_adapter(self, req: AdapterToggleRequest) -> None:
|
|
|
info(f"[Adapter] Toggling adapter: {req.name}, enable: {req.enable}")
|
|
|
|
|
|
if req.name not in self.configs:
|
|
|
info(f"[Adapter] Adapter not found: {req.name}")
|
|
|
raise ValueError(f"Adapter not found: {req.name}")
|
|
|
|
|
|
info(f"[Adapter] Current config: {self.configs[req.name]}")
|
|
|
# ...
|
|
|
```
|
|
|
|
|
|
### Q3: 前端页面空白?
|
|
|
|
|
|
**检查点**:
|
|
|
1. 浏览器控制台是否有错误
|
|
|
2. API 接口是否返回正确数据
|
|
|
3. 静态资源是否正确加载
|
|
|
|
|
|
```bash
|
|
|
# 检查管理后台路由是否正常
|
|
|
curl http://localhost:8080/admin
|
|
|
|
|
|
# 检查API接口
|
|
|
curl http://localhost:8080/v1/admin/system/status
|
|
|
```
|
|
|
|
|
|
### Q4: Python 依赖安装失败?
|
|
|
|
|
|
**解决方案**:
|
|
|
|
|
|
```bash
|
|
|
# 1. 升级pip
|
|
|
pip install --upgrade pip
|
|
|
|
|
|
# 2. 安装系统依赖(Ubuntu/Debian)
|
|
|
sudo apt install -y python3-dev libpq-dev gcc
|
|
|
|
|
|
# 3. 使用国内镜像
|
|
|
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
|
|
|
|
# 4. 单独安装有问题的包
|
|
|
pip install sqlalchemy psycopg2-binary --force-reinstall
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
## 性能优化建议
|
|
|
|
|
|
### 1. 配置缓存
|
|
|
|
|
|
#### Go
|
|
|
|
|
|
```go
|
|
|
// 添加配置缓存层
|
|
|
type CachedConfigService struct {
|
|
|
inner ConfigService
|
|
|
cache *config.Config
|
|
|
mu sync.RWMutex
|
|
|
ttl time.Duration
|
|
|
lastUpdate time.Time
|
|
|
}
|
|
|
|
|
|
func (s *CachedConfigService) GetCurrentConfig() *config.Config {
|
|
|
s.mu.RLock()
|
|
|
if time.Since(s.lastUpdate) < s.ttl {
|
|
|
defer s.mu.RUnlock()
|
|
|
return s.cache
|
|
|
}
|
|
|
s.mu.RUnlock()
|
|
|
|
|
|
// 缓存过期,重新加载
|
|
|
s.mu.Lock()
|
|
|
defer s.mu.Unlock()
|
|
|
|
|
|
s.cache = s.inner.GetCurrentConfig()
|
|
|
s.lastUpdate = time.Now()
|
|
|
return s.cache
|
|
|
}
|
|
|
```
|
|
|
|
|
|
#### Python
|
|
|
|
|
|
```python
|
|
|
# 添加配置缓存层
|
|
|
from functools import lru_cache
|
|
|
import time
|
|
|
|
|
|
class CachedConfigService:
|
|
|
def __init__(self, inner: ConfigService, ttl: int = 60):
|
|
|
self.inner = inner
|
|
|
self.cache = None
|
|
|
self.ttl = ttl
|
|
|
self.last_update = 0
|
|
|
self.lock = threading.RLock()
|
|
|
|
|
|
def get_current_config(self):
|
|
|
with self.lock:
|
|
|
if time.time() - self.last_update < self.ttl:
|
|
|
return self.cache
|
|
|
|
|
|
self.cache = self.inner.get_current_config()
|
|
|
self.last_update = time.time()
|
|
|
return self.cache
|
|
|
```
|
|
|
|
|
|
### 2. 数据库连接池
|
|
|
|
|
|
#### Go
|
|
|
|
|
|
```go
|
|
|
// 在 repository/database.go 中配置
|
|
|
db.SetMaxOpenConns(100)
|
|
|
db.SetMaxIdleConns(10)
|
|
|
db.SetConnMaxLifetime(time.Hour)
|
|
|
```
|
|
|
|
|
|
#### Python
|
|
|
|
|
|
```python
|
|
|
# 在 app/repositories/database.py 中配置
|
|
|
engine = create_engine(
|
|
|
config.database.database_url,
|
|
|
pool_size=10,
|
|
|
max_overflow=20,
|
|
|
pool_pre_ping=True,
|
|
|
pool_recycle=3600,
|
|
|
)
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
## 最佳实践
|
|
|
|
|
|
### 1. 配置管理
|
|
|
|
|
|
- 敏感信息(密码、token)使用 `********` 掩码显示
|
|
|
- 重要配置变更记录操作日志
|
|
|
- 配置验证在更新前进行
|
|
|
|
|
|
### 2. 适配器开发
|
|
|
|
|
|
- 实现完整的 `HealthCheck` 方法
|
|
|
- 处理网络超时和重试
|
|
|
- 使用连接池管理资源
|
|
|
|
|
|
### 3. 接口测试
|
|
|
|
|
|
- 使用参数化测试数据
|
|
|
- 测试失败时记录详细错误信息
|
|
|
- 清理测试产生的临时数据
|
|
|
|
|
|
### 4. 代码组织
|
|
|
|
|
|
**Go**:
|
|
|
- 按功能分层(api/internal/pkg/cmd)
|
|
|
- 使用接口定义依赖
|
|
|
- 错误处理要明确
|
|
|
|
|
|
**Python**:
|
|
|
- 使用类型注解
|
|
|
- 遵循PEP 8规范
|
|
|
- 合理使用异步
|
|
|
|
|
|
---
|
|
|
|
|
|
## 参考资源
|
|
|
|
|
|
### Go
|
|
|
- [Gin 框架文档](https://gin-gonic.com/docs/)
|
|
|
- [Go 并发模式](https://go.dev/blog/pipelines)
|
|
|
- [PostgreSQL 文档](https://www.postgresql.org/docs/)
|
|
|
|
|
|
### Python
|
|
|
- [FastAPI 文档](https://fastapi.tiangolo.com/)
|
|
|
- [SQLAlchemy 文档](https://docs.sqlalchemy.org/)
|
|
|
- [Pydantic 文档](https://docs.pydantic.dev/)
|
|
|
|
|
|
---
|
|
|
|
|
|
**文档结束**
|