2025 年底,星辉广告系统的同步引擎到了必须重构的临界点。原因是每个上游供应商的 API 都需要一份独立的适配代码——新增一个供应商,就要写一遍 HTTP 请求、参数拼装、鉴权、分页、字段映射、写入逻辑。当时系统接了 5 家上游,对应的同步代码已经膨胀到 3000 多行,每接一家新供应商,开发周期至少两到三周。
而业务那边又在催:下个月要再接入两家。
这不是一个技术难题,这是一个架构问题——我们把应该配置化的东西写死在了代码里。
先看清楚 V1 为什么撑不下去了:
V1 的同步代码大致长这样——每个供应商一个文件,每个文件里重复着 80% 相似的逻辑:
// 供应商 A 的同步代码
func SyncSupplierA() error {
url := "https://api.supplier-a.com/v1/report"
headers := map[string]string{
"Authorization": "Bearer " + cfg.APIKeyA,
}
params := buildParams(date, "daily")
resp, _ := httpGet(url, headers, params)
rows := parseResponseA(resp) // 每个供应商的解析逻辑不同
for _, row := range rows {
db.Exec("INSERT INTO stats ...", row)
}
}
// 供应商 B:同样的结构,不同的 URL、鉴权、解析
func SyncSupplierB() error { ... } 这种模式的问题很明显:
重构的核心思路很简单:把变化的抽成配置,把不变的固化成引擎。
经过几轮迭代,最终确定了三级配置结构:
V2 同步引擎架构:
┌─────────────────────────────────────────────┐
│ 同步引擎(通用逻辑) │
│ HTTP 请求 / 鉴权 / 分页 / 重试 / 错误处理 │
│ 字段映射 / 数据清洗 / 写入 │
└──────────────────┬──────────────────────────┘
│
读取配置 │
▼
┌─────────────────────────────────────────────┐
│ 一级:供应商配置(supplier_sync_config) │
│ └─ base_url、鉴权方式、请求频率 │
│ │
│ 二级:端点配置(sync_endpoint) │
│ └─ API路径、参数风格、分页方式 │
│ │
│ 三级:字段映射(sync_field_mapping) │
│ └─ 上游字段名 → 系统字段名、类型转换 │
└─────────────────────────────────────────────┘ 定义"怎么连"——每个上游 API 的基本连接信息:
// 供应商级配置表(简化)
supplier_sync_config {
id INT PRIMARY KEY,
name VARCHAR(64), // 供应商名称
base_url VARCHAR(255), // API 基础地址
auth_type ENUM('none','header_auth','query_key'),
auth_key VARCHAR(128), // 鉴权字段名
auth_value VARCHAR(256), // 鉴权密钥(加密存储)
interval_m INT, // 同步间隔(分钟)
enabled TINYINT(1),
} 定义"请求什么"——数据在哪个 API 路径、怎么分页:
// 端点级配置表(简化)
sync_endpoint {
id INT PRIMARY KEY,
supplier_id INT, // 关联供应商
name VARCHAR(64), // 端点名称(daily/unit/hourly...)
path VARCHAR(255), // API 路径
param_style ENUM('query','json_body'),
page_param VARCHAR(64), // 分页参数名
page_size INT, // 每页大小
date_param VARCHAR(64), // 日期参数名
date_format VARCHAR(32), // 日期格式
} 定义"字段怎么对"——上游返回的 JSON 字段名映射到系统字段:
// 字段映射配置表(简化)
sync_field_mapping {
id INT PRIMARY KEY,
endpoint_id INT, // 关联端点
source_key VARCHAR(128), // 上游 JSON 中的字段名
target_key VARCHAR(128), // 系统数据库字段名
data_type ENUM('string','int','float','date'),
transform VARCHAR(64), // 可选:转换函数名
required TINYINT(1), // 是否必填
} 配置建好了,引擎要做的就是通用的"读取配置 → 组装请求 → 调用 API → 解析响应 → 映射字段 → 写入库":
// V2 引擎核心流程(简化伪代码)
func SyncByHTTPAPIV2(endpointID int) error:
// 1. 读取三级配置
supplier = loadSupplierConfig(endpointID)
endpoint = loadEndpointConfig(endpointID)
mappings = loadFieldMappings(endpointID)
// 2. 按日期范围分批执行
for each day in dateRange:
// 3. 组装请求参数
params = buildParams(endpoint, day)
// 4. 发送 HTTP 请求
resp = httpGet(supplier.baseURL + endpoint.path,
auth=supplier.auth,
params=params)
// 5. 解析响应(通用 JSON 解析)
rows = parseResponse(resp)
// 6. 字段映射
for each row in rows:
mapped = applyMappings(row, mappings)
// 7. 清洗 + 写入
cleaned = clean(mapped)
writeToDB(cleaned)
// 8. 记录同步状态
saveSyncLog(endpointID, date, status) 这里面每一步都是通用的——API 响应格式不同的场景,通过 动态 JSON 解析器来兼容:引擎自动识别响应是列表还是嵌套结构,根据字段映射配置提取数据。
V2 上线后的变化是立竿见影的:
| 指标 | V1 | V2 |
|---|---|---|
| 新增供应商周期 | 2-3 周 | 1-2 天 |
| 供应商相关代码行数 | ~3000 行(5 家) | ~200 行引擎 + 配置表数据 |
| 新增供应商修改文件数 | 4-6 个 | 0 个(只需 INSERT) |
| 同步失败率 | ~5%(各供应商处理不一致) | <1%(统一错误处理+重试) |
更重要的是,配置化带来的是能力边界的变化。V1 时代,新增供应商是一个开发任务——要排期、要测试、要部署。V2 时代,新增供应商是一个运营任务——运营同学在管理后台配好表就能上线。这彻底改变了团队的协作模式。
这次改造并非一帆风顺。记录几个关键的教训:
data.stats.impressions)。后来加上了 source_key 支持点号路径。page 参数,有的用 offset,有的用游标。最终方案是在 sync_endpoint 表加了一个 page_style 字段来区分。"1,234"(带逗号的字符串)和 "N/A" 在类型转换时频繁报错。最终加了一层"清洗函数"作为可配置的转换管道。从 V1 到 V2 的核心变化:
硬编码 → 配置化
每个供应商一个文件 → 三级配置表
新增 = 开发任务 → 新增 = 运营任务
3000 行供应商代码 → 200 行通用引擎
2-3 周接入周期 → 1-2 天
三条经验:
1. 把变化的抽成配置,把不变的固化成引擎
2. 先做 80%,剩下的迭代补齐
3. 配置化改变的不只是代码结构,还有协作模式 这次改造最大的收获不是代码量减少了,而是系统能力的边界扩展了。当一个之前需要两到三周才能完成的事情变成一两天就能搞定,你的业务选择空间也随之变大。这大概就是架构设计的价值——它不直接产生功能,但它决定了你能以多快的速度产生功能。
💬 评论