示例代码均为匿名化改写,用于说明模式;服务端仍需确认资格、金额和最终状态。
先加载金额,再开放提交
可退金额通常受订单状态、已消费部分和后端规则影响,应由服务端返回。申请页先加载金额并显示错误状态,只有金额可用且理由有效时,才允许用户进入最终确认。
常见理由可以是固定选项;“其他”理由应修剪首尾空格、限制长度并拒绝空白输入。这些规则可以放在独立支持层,而非只依赖某个文本框的 UI 验证。
enum RefundReason: Equatable, Sendable {
case quality
case schedule
case other(String)
}
enum RefundReasonError: Error {
case empty, tooLong
}
func validated(_ reason: RefundReason) throws -> RefundReason {
guard case let .other(text) = reason else { return reason }
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { throw RefundReasonError.empty }
guard trimmed.count <= 200 else { throw RefundReasonError.tooLong }
return .other(trimmed)
}
中文输入法处于组合输入时,文本长度可能尚未稳定;输入过程中只显示提示,待组合提交后再做最终长度校验,避免打断输入。
保护异步表单状态
每次请求都记录请求标识和当前登录会话。只有仍属于当前页面请求和账号的结果才更新退款金额。切换订单、退出账号或页面取消后,迟到的响应应静默丢弃。
提交时用状态锁避免重复操作。埋点应区分“用户尝试提交”和“服务端成功受理”;收到结果后再次校验会话身份,再决定展示成功或失败。
客户端提交锁可以阻止重复点击,但可靠的防重还需要服务端幂等键;两端职责不同,不能只靠按钮禁用。
申请和进度属于两条状态流
申请页负责输入、金额和提交;进度页负责撤销、确认处理结果和异议说明。进度操作应先要求确认,再依据服务端响应更新状态。弹窗呈现和页面导航可以独立建模,避免塞进同一个大型视图。
可验证的边界
- 金额读取失败时不能继续申请。
- 自定义理由经过修剪和长度限制。
- 账号切换后旧结果不覆盖当前页面。
- 撤销或同意操作失败时不显示成功状态。
