这次工作最初是一份接口问题清单:找到对应的 iFlow,用已有请求触发,结合 Trace 和 S/4 程序逻辑,判断字段为什么没有返回、应该由哪一侧调整。
随着 Bruno 请求集合和历史接口材料补齐,FAQ 接口的工作逐渐明确:先对齐已有可用接口的调用方式,再把登录逻辑从业务 iFlow 中抽出,交给公共 iFlow 管理。这里记录这条已经完成验证的改造链路,以及过程中几个容易误判的地方。
文中的流程名称使用 FAQ_FLOW 和 TOKEN_FLOW 代称。租户地址、凭据别名、业务设备号和原始 Trace 均不公开。代码片段用于说明实现原则,不是可以直接导入任意租户的完整部署包。
从“字段没返回”开始,先建立证据边界
会话前半段涉及多个接口:设备档案、特征值和 FAQ。排查材料既有接口清单,也有数据库查询结果、表截图和业务顾问对记录出现时间的反馈。
这些材料回答的问题并不相同。
| 材料 | 可以确认的事情 | 不能单独推出的结论 |
|---|---|---|
| Bruno 请求 | 请求路径、方法、参数、认证方式 | 当前部署的 iFlow 一定按相同方式调用 |
| CPI Trace | 某一次运行中具体步骤的报文 | 所有调用都经过同一分支 |
| S/4 表记录 | 查询时该环境中存在这些数据 | 这些记录一定由某次接口调用创建 |
| ABAP 源码 | 被阅读版本的取数及处理逻辑 | 另一 client 的数据和运行结果必然相同 |
| 最终 HTTP 响应 | 调用方实际收到的内容 | 中间每个步骤都正确 |
例如,返回中没有 characteristics ,需要继续区分:后端没查到数据、程序没有填充字段、映射没有传递,还是序列化改变了空值的表达方式。
又例如,顾问说“下午之后才出现第一条记录”,这能帮助缩小时间窗口,但不能直接证明记录由我的测试创建。还需要把请求内容、运行时间、消息关联标识和后端写入路径对起来。
因此,这篇文章不把前面所有问题归结为“S/4 的问题”或“CPI 的问题”。后面 FAQ 的修复有独立的请求、部署和运行证据,其结论只覆盖这条链路。
Bruno 提供了认证协议,不能从旧 iFlow 的名字猜
补充材料中的请求集合给出了当前可用的 FAQ 调用方式:先发 JSON 登录请求,取出响应里的 token ,再把它放进业务请求的 Bearer header。
登录请求的字段名有大小写要求:
{
"Username": "<username-from-secure-store>",
"Password": "<password-from-secure-store>"
}
这不是看到“获取 Token”几个字,就可以套用的 OAuth2 client-credentials grant。旧项目里常见的 clientId/clientSecret 、 access_token 或嵌套字段,都不能代替实际接口协议。
第一阶段已经让 FAQ 正确调用了这个接口,但登录仍发生在 FAQ 自己的 Groovy 脚本中,每次请求都重新获取 token。第二阶段的目标才是本次重点:把这段已经验证的登录逻辑移入公共流,同时保持业务响应。
准备复用的公共流来自另一个项目。下载当前工件后,发现了几个明确的遗留问题:
- 登录 body 使用旧项目字段,响应解析读取旧的嵌套字段。
- Data Store 的读取名称和写入名称不同。
- 过期时间来自配置的固定分钟数,没有绑定实际签发时间。
- 缓存命中分支没有像刷新分支一样设置返回 header。
- 部分步骤把 token 当作 body 返回,不符合这次公共接口约定。
这些问题解释了为什么不能只替换 URL 就部署。可以复用它的 Data Store、Router、Local Integration Process 结构,但必须重新核对协议、存储和分支出口。
修改从当前工件和可回退版本开始
这次没有拿旧的本地 ZIP 接着改,而是先从 QAS 下载两个当前工件,再保存未修改版本。
| 流程 | 修改前版本 | 本次备份版本 | 新运行版本 |
|---|---|---|---|
| FAQ_FLOW | 1.0.10 | 1.0.11 | 1.0.12 |
| TOKEN_FLOW | 1.0.1,尚未部署 | 1.0.2 | 1.0.3 |
保存时还遇到了编辑锁。API 返回:
Cannot update the artifact as it is locked.
退出编辑后第一次重试仍失败,用户明确完成解锁后,保存才成功。随后重新下载并比较内容,确认备份没有夹带新的业务改动。
这个步骤同时保护两件事:回退时有准确的原始版本,修改时也不会覆盖浏览器中另一份尚未确认的编辑。
公共流管理 Token,FAQ 保留业务路径
最终调用关系如下。图中的步骤名称是概念说明,对应实际部署中的脚本、Data Store、Router 和 ProcessDirect 调用。
客户端
│
▼
FAQ_FLOW:检查并保存 FAQ 路径
│
├── ProcessDirect ──► TOKEN_FLOW
│ │
│ ├─ 读取加密缓存
│ ├─ 检查作用域、格式和有效期
│ │ ├─ 有效:复用
│ │ └─ 无效:登录 → 计算有效期 → 写入缓存
│ └─ 返回 access_token header,body 为空
│
▼
恢复 FAQ 路径,设置 Authorization
│
▼
调用 FAQ 接口
│
▼
清理认证 header,原样返回业务响应
FAQ 在调用公共流前保存经过校验的路径。这样,即使公共流为了读取和写入缓存而使用了消息 body,也不会改变业务请求的地址。
公共流两个成功出口采用同一约定:返回 access_token header 和有效截止时间 header,body 为空。FAQ 收到后生成 Authorization: Bearer ... ,随即删除内部 token header;正常和异常出口都会清理认证信息。
这里的“原样返回”指不增加业务字段转换、排序或 JSON 包装。接口认证方式改变,不应顺便改变调用方依赖的响应结构。
有效期取实际签发值,再减十分钟
实际登录响应包含 tokenValidTo ,JWT 中包含 exp 。本次观察到的 token 有效期是七天,但代码没有把七天写成常量。
实现选用 JWT 的绝对 UTC 过期时间计算缓存截止时间:
// Use the issuer's absolute expiry, not a fixed duration from configuration.
long actualExpiresAtEpochMillis = expSeconds * 1000L
long expiresAtEpochMillis = actualExpiresAtEpochMillis - 600000L
// Refresh at the cutoff itself; do not wait until the actual token expiry.
boolean reusable = nowEpochMillis < expiresAtEpochMillis
保留十分钟余量,目的是让 token 在被取出之后,还能留出业务调用和网络传输的时间。到达截止点时必须刷新,所以比较条件是严格的小于。
加密 Data Store 中保存的记录包含以下字段:
| 字段 | 用途 |
|---|---|
| token | 实际使用的 token |
| actualExpiresAtEpochMillis | 签发方声明的实际过期时间 |
| expiresAtEpochMillis | 减去十分钟后的缓存截止时间 |
| credentialAlias | 标识缓存对应的凭据配置 |
| authenticationUrl | 标识缓存对应的登录服务 |
| schemaVersion | 区分缓存结构版本 |
读取缓存时不仅判断“还没过期”,还校验作用域和字段之间的一致性。缺失、格式损坏、旧格式、配置不匹配或已过期,都按缓存未命中处理。
新签发的 token 如果已经不足十分钟有效时间,会直接报错,而不是写入一条马上失效的缓存。命中缓存时也不会重新计算“从现在开始七天”,因此重复访问不会无限续期。
这里解码 JWT 只是为了读取从可信 HTTPS 登录响应取得的 exp ,没有实现 JWT 签名验证。它不能被当成验证外部调用方 token 的鉴权模块。
凭据和缓存各自有不同的保护边界
用户名和密码从 CPI Secure Store 读取。登录请求和完整登录响应保留为脚本局部变量,没有写入交换属性或日志附件;只有供加密 Data Store 写入的缓存记录进入消息 body。
Data Store 使用本地作用域、加密存储,且不保存消息 headers。读写使用同一个名称和 entry ID。
不过,“加密存储”不等于“运行中永远不可见”。管理员开启 Trace 后,临时 body 和出站 Bearer header 仍可能被记录。为确认缓存是否工作,没有必要导出这些内容。
这次增加了两个不含 token 的 MPL 自定义属性:
SAGTokenCache:记录MISS、REFRESHED或HIT。SAGTokenExpiresAt:记录有效缓存截止时间。
这样可以在 INFO 级别确认分支行为,同时把凭据和完整 token 排除在排障报告之外。
保存成功之后,还要核对真正保存了什么
上传新包并保存版本后,回读发现一个细节:FAQ 中已经移除了两个认证参数的定义和 BPMN 引用,但 CPI 导出的 parameters.prop 仍保留原配置值。
公共 token 流的配置行顺序也被重新整理了。如果只比较 ZIP 哈希或文本行顺序,很容易把这些变化与业务修改混在一起。
本次核对采用了更窄的规则:
- 脚本和 BPMN 按实际内容逐字节比较。
- Java Properties 按键值比较,忽略已确认的注释和顺序变化。
- 对残留配置键逐一确认:值与备份相同,且新定义和 BPMN 都不再引用。
- 再通过配置 API 核对有效 endpoint、认证 URL 和凭据别名。
不能因为发现一项导出差异,就把所有差异都当作“平台自动处理”。也不能因为配置文件里还留着一个字段,就认为运行逻辑仍在使用它。
部署顺序是先公共流、确认 STARTED ,再部署 FAQ。公共流启动成功只是下一步测试的前提,还不能证明整个业务链路可用。
测试覆盖边界,也覆盖真正的运行链路
本地 Groovy 测试使用模拟 Message、Secure Store 和 HTTP 连接,检查登录协议与缓存判断,包括:
- 首次未命中、有效缓存命中、过期后重新登录。
- 截止时间前一毫秒、恰好截止、截止后一毫秒。
- 损坏缓存、作用域不符、旧存储格式和不一致的有效期字段。
- token 缺失、格式错误、剩余时间不足以及登录 HTTP 错误。
- FAQ 路径恢复、body 清理、认证 header 清理及业务响应保留。
这些测试通过后,才使用真实 QAS 链路验证 Data Store 和 ProcessDirect。实际结果如下:
| 调用 | HTTP | 公共 Token 流行为 | FAQ 与公共流 MPL |
|---|---|---|---|
| 第一次 | 200 | MISS → REFRESHED | 均为 COMPLETED |
| 第二次 | 200 | HIT | 均为 COMPLETED |
| 第三次 | 200 | HIT | 均为 COMPLETED |
三次观察到的缓存截止时间相同。两次命中没有重新登录,也没有把有效期向后延长。客户端响应中没有泄露认证或内部 token headers。
实际七天后的自然过期没有等待完成;过期刷新与精确边界由本地测试验证。这个限制需要写清楚,不能把模拟时间边界测试描述成“线上已连续观察一周”。
“返回结果一样”也需要拆开验证
验收中最值得保留的一个细节,是新旧响应比较第一次失败了。
新流程返回 HTTP 200,而且与当次直接调用后端取得的完整 JSON 一致。但是,与修改前保存的基线相比,严格的数组逐项比较失败。
继续检查后,差异只在 applianceCodes :共 10,863 个元素,数量和全部值相同,排列顺序不同。其他字段没有差异。
因此,验收结论必须分别表述:
- 与当次直接调用后端的响应相比,完整解析后的 JSON 一致。
- 与较早基线相比,业务值一致,但一个数组的顺序不同。
- 仅在测试中对该数组排序后,完整结果一致。
生产 iFlow 没有因此增加排序步骤。直接后端响应也呈现了相同结果,说明这次观察到的顺序差异并非 CPI 添加的转换。
如果调用方依赖某个数组的顺序,那仍然需要单独确认接口契约。这次比较只能证明哪些内容相同、哪些内容不同,不能擅自把数组顺序宣布为无关紧要。
这次改造留下的边界
完成后,FAQ 负责请求路径和业务调用,公共流负责登录与缓存。旧版本被保留,两个新版本完成回读核对和真实调用验证,维护脚本中的注释也统一为英文。
缓存仍有明确边界:并发首次未命中可能触发多次登录,没有实现跨节点的单次刷新锁;后端提前吊销 token 也没有增加自动重试。这些都没有被包装成已经解决的问题。
这次复盘最有价值的部分,是每一层结论都能找到对应证据:协议来自已有请求,缓存结构来自当前工件检查,过期规则来自实际 token,流程可用性来自关联 MPL,而响应兼容性来自完整结果比较。下一次再遇到“接口没返回字段”,就可以沿着同样的证据链缩小范围,而不必一开始就决定由哪个系统背锅。