接入 Startlux-Decision 时,先改配置还是先写适配层,取决于差异的性质,而不是团队习惯。通常的做法是:先把两边的字段差异列成一张表,凡是命名、层级、简单别名能对上的,先改配置并用一条固定样例请求验证;只有出现语义转换、单位换算、默认值补齐、字段裁剪这类配置表达不了的情况,才在调用边界写一层 adapt 函数。反过来先写适配层也跑得通,但很容易把一处配置就能解决的问题固化成需要长期维护的代码。
如果两边只是字段名、层级不同,类型和取值域一致,缺失语义也一致,建议优先在配置里做映射,改动小、回退快;如果字段含义、单位、枚举口径或缺失语义不一致,或者需要给下游补默认值、裁掉多余字段,就写最小适配层,把差异集中在一个函数里。两种做法都要留开关和回退路径,验证方式是拿同一份请求样例对比映射前后的结构,而不是上线后再凭日志观察。
盘点现有服务与 Startlux-Decision 的字段差异
先把现有服务的请求体和响应体各取一条真实样例,再对照 Startlux-Decision 侧期望的输入输出结构,逐个字段填表。判断标准很简单:命名不同但语义相同,属于命名问题,配置能解决;类型、取值域、单位、缺失语义有一项对不上,就是语义问题,配置通常解决不了。下面这张表用占位字段演示填法,实际字段名按你两侧的接口替换。
| 字段名 | 类型 | 取值域 | 缺失处理 | 能否配置映射 |
|---|---|---|---|---|
| user_id / uid | string / string | 同一套 ID | 都视为必填 | 能,别名映射即可 |
| order_status / status | string / string | 现有服务用 paid、created;对端用 1、2 之类的编码 | 都必填 | 不能,需枚举转换,属于语义差异 |
| amount | integer / integer | 现有服务为分,对端为元 | 都必填 | 不能,需单位换算 |
| event_time | string / integer | ISO 时间串与毫秒时间戳 | 现有服务可缺省 | 不能,需解析加默认值 |
| tags | string 逗号分隔 / array | 内容相同 | 允许为空 | 不能,需结构转换 |
| source_channel | string / 无对应字段 | 现有服务内部专用 | 对端不接受 | 需要裁剪 |
填完表后,如果「能否配置映射」这一列全是「能」,就继续看下一节;只要出现一到两条「不能」,并且这些字段参与决策,就基本可以确定需要适配层,而不是继续堆配置规则。
先尝试最小配置映射
配置映射只做一件事:把现有服务的字段名和层级,映射到 Startlux-Decision 期望的位置。它适合放在调用 Startlux-Decision 之前的那层接入配置里,比如网关或服务自身的接入配置文件,不适合散落在业务代码各处。下面是一份通用的 YAML 骨架,字段都是占位,按实际接口替换。
startlux:
mapping:
input:
# 左侧为 Startlux-Decision 期望字段,右侧为现有服务字段
user_id: uid
order_status: status
amount: amount
event_time: event_time
output:
# 决策结果回写时的字段位置
decision_code: result.code
decision_reason: result.reason
missing_policy:
# 仅在两侧缺失语义一致时使用
event_time: null
骨架中的 mapping 只表达改名和取路径,不要在这里塞枚举转换或单位换算,否则后面很难从配置里看出真实语义。验证方式是用同一条请求样例走一遍映射,检查映射后的请求体字段齐全、类型正确,再把对端返回按 output 映射回来,确认下游解析器不报错。如果这一步就出现字段对不齐、类型报错或返回结构与预期不符,说明配置映射的边界到了,转下一节。
需要适配层时写最小转换函数
适配层不是官方接口,只是你在调用边界自建的一对函数,作用是把差异挡在外面。建议只写两个函数:adapt_input() 负责把现有服务的请求转成 Startlux-Decision 能接收的结构,adapt_output() 负责把返回结果转回现有服务的字段。转换逻辑尽量保持在十几行内,超过这个规模通常意味着差异没想清楚。
# 伪代码,非官方接口,仅示意结构
STATUS_MAP = {"created": 1, "paid": 2, "closed": 3}
def adapt_input(raw):
out = {}
out["user_id"] = raw["uid"]
out["status"] = STATUS_MAP[raw["status"]] # 枚举转换
out["amount"] = raw["amount"] // 100 # 单位换算,分转元
out["event_time"] = to_millis(raw.get("event_time")) or now_millis() # 补默认值
return out # 其余字段不放入,等于裁剪
def adapt_output(resp):
result = resp.get("result") or {}
return {
"decision_code": result.get("code"),
"decision_reason": result.get("reason", ""),
}
写完函数不要直接接上联调,先用手写样例做断言。样例取自你之前采集的真实请求,把输入和期望输出都写死,跑一遍看是否一致。下面这段断言可以放在本地测试文件里执行,能通过再接第二步。
sample_in = {"uid": "u-1", "status": "paid", "amount": 1000, "event_time": None}
assert adapt_input(sample_in)["user_id"] == "u-1"
assert adapt_input(sample_in)["status"] == 2
assert adapt_input(sample_in)["amount"] == 10
sample_out = {"result": {"code": "pass", "reason": "ok"}}
assert adapt_output(sample_out)["decision_code"] == "pass"
比较改配置与写适配层的维护成本
两种做法不是谁更高级,而是成本落在不同地方。配置改动上线快、回退容易,但当差异数量变多时,配置会变成一堆难读的规则;适配层把逻辑集中在一处、便于测试,但多了一份需要随两侧接口变化同步维护的代码。可以按下面的表做取舍,哪一列落在你这边,就偏向对应做法。
| 判断项 | 偏向改配置 | 偏向写适配层 |
|---|---|---|
| 变更频率 | 两侧字段基本稳定,偶尔改名 | 任一侧字段经常增减或改口径 |
| 字段差异数 | 集中在少数几个字段 | 差异字段多,且分散在请求与响应两侧 |
| 是否有状态 | 无状态,单次转换即可 | 需要缓存映射、维护配置版本或做重试 |
| 回退难度 | 改回原值或关掉映射即可 | 要下线代码或切分支,回退成本偏高 |
| 测试成本 | 用一条样例请求人工比对即可 | 需要断言集覆盖枚举、单位、默认值分支 |
如果表里偏向适配层的项超过两三条,建议直接写适配层,别先用配置硬撑;只有一两条偏向适配层,可以先用配置映射上线,同时把这几条记下来,等差异扩大再重构成适配函数,避免过早引入代码。
设置联调和回退检查点
无论走哪条路,都要在接入处留一个能立刻切回旧链路的开关,并让日志能看出请求走的是哪一边。开关建议放在接入配置文件里,而不是代码常量。
startlux:
enabled: false
route: legacy # legacy 走旧链路,startlux 走新接入
日志至少记录这几个字段:trace_id、route、enabled、adapt_ms(适配耗时,用于排查卡顿)、missing_field(本次被补默认值或裁剪的字段名)。联调时先用 route=startlux 跑一条样例,看日志里的 missing_field 是否和差异表一致;不一致说明映射或转换写漏了。回退步骤按顺序执行:先把 enabled 置为 false,再重载配置或重启接入层,然后确认日志里 route 回到 legacy,最后重放同一条样例确认旧链路返回正常。
# 验证命令占位,路径与字段按实际服务替换
curl -s -X POST http://127.0.0.1:8080/api/decision \
-H 'Content-Type: application/json' \
-d '{"trace_id":"demo-1","uid":"u-1","status":"paid","amount":1000}' \
| jq '.route, .result'
需要说明的边界是:开关只负责链路切换,不代表性能或稳定性改善;回退后旧链路的行为仍取决于旧服务本身,接入层无权保证。若两侧字段差异持续扩大,回退只能作为临时手段,最终还是要把适配逻辑和差异表一起维护起来。