公交请求韧性·02:HarmonyOS 幂等键与重复反馈隔离【共创稿事节】
在公交到站页的“到站信息不准确”反馈提交中,最难的往往不是画出一个按钮,而是保证按钮背后的业务事实始终可靠。乘客连点提交,或者弱网导致响应丢失,就可能出现重复反馈或重复扣减服务端配额。这个问题如果在原型阶段不拆开,联调以后就会成为难以复现的状态错乱。

需求从哪里变复杂
公交到站纠错属于写操作。用户提交了“车辆提前”反馈,网络可能已经把数据交给服务器,但响应包在回程时丢失。此时简单重发同一个 POST,会让服务器看到第二次动作;仅把按钮置灰无法处理 App 重启或中途重试。
“禁用按钮只能减轻连点,真正保证一次业务动作需要客户端请求标识与服务端幂等存储协同”是这篇最重要的设计判断。单看成功路径,公交到站页的“到站信息不准确”反馈提交可以靠若干事件回调工作;但只要用户中途修改、取消或重复操作,若没有清晰约束,就无法确定最终输出究竟属于哪一次输入。工程拆分必须以这些不确定性为起点。
我们希望得到的不是一张孤立的效果图,而是一条可追溯的处理链。整个过程拆为创建动作、固定 actionId、发起请求、重试同键、服务端去重。每一段只承诺自己负责的结果,下一段不靠猜测前一步的内部状态。对请求幂等而言,这种清晰分工也是回归测试能够写得具体的前提。
幂等键必须代表一次业务意图,而不是一次网络连接。一次反馈可以经历多次请求尝试,但都带同一个 Idempotency-Key;用户明确又发起一条新反馈时,则应创建新的键。这样才能区分重试与新动作。
先固定数据边界
实施选择是:为一次用户动作分配 actionId,重试必须沿用同一标识,只有开启新动作才更新。这里不能倒过来,先把组件和请求写完,再向现有代码里硬塞判断。规则入口只接收必要数据,输出也尽量可序列化。这样在没有设备的环境下仍能对核心逻辑做确定性检查。
从“创建动作”走到“固定 actionId”时,不要直接复用页面当前显示的全部状态。界面可能正在更新,用户也可能继续操作。此处应只传递本阶段约定的数据,并记录这份数据属于哪次动作,减少跨阶段读到不同版本的风险。
“固定 actionId”阶段很容易被认为已经完成了整个功能,其实它只提供一个前提。只有进入“发起请求”之后,才能知道前一步是否真的满足业务要求。把这个检查写成显式返回值,比依赖某个内部字段在恰当时刻改变更稳健。
当路径执行到“发起请求”,先区分用户仍在编辑、数据已经校验和动作已经确认三种事实。随后执行“重试同键”时,使用同一份中间结果才能避免旧数据回流。日志或测试也应记录阶段,而不是只看最终页面有没有消失。
客户端只负责稳定携带动作标识。服务端必须在具有一致性约束的存储中登记这个标识,并对重复请求返回相同处理结果,才能保证副作用只执行一次。没有服务端协作,任何客户端代码都无法单方面实现全链路幂等。

让代码成为规则的证据
下面给出可以独立检查的关键实现。代码只覆盖本篇要证明的核心行为;涉及工程权限、服务端协议或构建设置的条件已在上下文列明。它应被放入明确的模块中,而不是把所有业务逻辑堆进 ArkUI build()。
interface Action { id: string; reason: string; status: string }
interface RequestHeader { 'Idempotency-Key': string }
class FeedbackCoordinator {
private pending: Action | null = null
begin(id: string, reason: string): Action {
if (this.pending !== null) return this.pending
this.pending = { id: id, reason: reason, status: 'pending' }
return this.pending
}
requestHeader(): RequestHeader {
if (!this.pending) throw new Error('no active action')
return { 'Idempotency-Key': this.pending.id }
}
done(): void { this.pending = null }
}
// id 必须由安全的唯一 ID 生成器产生,服务器亦须按该键去重。
标识生成应采用成熟的随机标识方案,并明确其生命周期。示例函数接收外部生成的 id,是为了突出绑定规则;实际接入要选用当前 SDK 中可核实的安全随机或 UUID 方案,不能用自增整数冒充全球唯一。
真正发送反馈时,必须从 begin 得到同一个 Action,并把 requestHeader() 放进每一次重试的请求头。下面的 request 调用只构成客户端协作,服务端仍须实现按键去重。
import { http } from '@kit.NetworkKit'
async function postFeedback(url: string, action: Action,
coordinator: FeedbackCoordinator): Promise<number> {
const req = http.createHttp()
try {
const result = await req.request(url, {
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json',
'Idempotency-Key': coordinator.requestHeader()['Idempotency-Key'] },
extraData: JSON.stringify({ reason: action.reason })
})
return result.responseCode
} finally { req.destroy() }
}
代码中的关键判断来自业务约束,而不是某个屏幕比例。以“发起请求”为界,输入应先满足所属阶段的前置条件;当判断失败时,返回错误或保留状态比直接抛出无法解释的页面状态更可控。对需要 UI 反馈的部分,随后由组件转换为明确的文案。
如果把上述核心逻辑接到一个 ArkUI 页面,推荐组件仅处理状态投影和交互事件:例如 TextInput.onChange 接收新的字符串,Button.onClick 触发一次显式动作,得到结果后再设置 @State 驱动展示。对于不是输入框的请求幂等功能,则采用相同的单向数据边界而不强行引入 TextInput。

错误不应藏在界面后面
最应该提前考虑的风险是:不能将当前毫秒时间戳直接当作全局唯一且不可碰撞的幂等凭证;不要将服务端一致性伪装成客户端单方面实现。这句话不是发布时随手附加的注意事项,而是编码时必须坚持的限制。只要异常路径缺乏明确处理,下一次回归就可能把一个本来可恢复的状态变成难以判断的失败。
测试构造首次请求处理成功但响应丢失,随后用同一 key 重试。检查服务端只有一条反馈记录,并且两次响应指向同一个业务结果。再用新 key 发送新的反馈,确认其确实创建了第二条记录。
状态验收。 在公交到站页的“到站信息不准确”反馈提交中,专门制造一个尚未完成的状态:乘客连点提交,或者弱网导致响应丢失,就可能出现重复反馈或重复扣减服务端配额。观察模块是否明确阻止下一动作、是否保留用户已经输入的有效内容,以及重新操作后是否会得到可解释的新结果。这个检查的价值在于不把一次成功操作当作稳定性的全部证据。
逆序验收。 把正常操作反过来执行,例如先触发重试同键,再补齐发起请求所需要的条件。若界面没有把错误留在错误位置,或者隐式使用上一轮结果,就说明阶段约束并未完全落实。真正的测试应覆盖操作顺序变化,而非只按产品示例步骤点击。
重复验收。 连续两次执行重试同键,再执行服务端去重,检查第二次动作是新意图还是前次动作的继续。对于请求幂等尤其要明确重复处理规则;“按钮点了两次”是用户行为事实,并不自动意味着应当产生两份业务副作用。
错误恢复。 在创建动作成功、创建动作尚未完成的位置构造异常。应能说明数据暂存在哪里,用户看到何种反馈,是否允许重新触发,以及什么条件下必须停止。不要为了追求一张好看的页面而隐藏正在等待确认的真实状态。
怎样确认这套设计可靠
这一层解决“是否重复执行”,下一层解决“何时允许重试”。幂等与退避不是互相替代的关系:前者约束业务副作用,后者保护系统容量和用户等待体验。
本篇可用以下结果作为验收目标:一次操作的三次网络尝试在服务端必须最终对应同一业务记录。检验时先从固定输入入手,再加入中途切换、重复操作和失败恢复。可解释的错误结果也属于正确实现的一部分;没有必要为了让每个步骤都显示绿色而悄悄跳过不满足的前置条件。
还有一个容易被忽视的协作问题:页面状态、领域规则和最终输出往往由不同代码文件甚至不同开发者维护。要让改动可控,必须为请求幂等明确最小接口,规定哪些数据可以跨模块传递、哪些只是展示层的临时变量;代码评审只要看到两处重复决定同一事实,就需要回到单一来源原则。
调试不应从增加大量打印开始。先把创建动作 → 固定 actionId → 发起请求 → 重试同键 → 服务端去重按顺序列出来,每次只在边界处记录不含个人信息的阶段与结果类别。这样即使没有真机性能指标,仍能通过构造输入与检查返回值定位哪一步发生分歧;反过来,如果只看最终页面颜色,就难以证明中间没有错误。
关于不能将当前毫秒时间戳直接当作全局唯一且不可碰撞的幂等凭证;不要将服务端一致性伪装成客户端单方面实现这一风险,建议把它写进实际测试用例的名称和断言,而不是只放在代码注释。注释会随着重构失效,测试中的输入与预期结果更容易暴露行为变化。测试本身同样要避免依赖随时变化的网络与时间,否则会把产品故障和测试环境偶然因素混在一起。

这类问题的适用边界同样清楚:禁用按钮只能减轻连点,真正保证一次业务动作需要客户端请求标识与服务端幂等存储协同。当具体 SDK、设备能力或服务端协议发生变化,应重新查看对应的官方接口约束,再重新跑上述场景验证,而不是根据一张示例图推断系统行为。
官方资料
更多推荐

所有评论(0)