先说结论:表单提交丢数据,九成不是后端接口挂了,而是前端提交链路和入库逻辑缺了可靠性设计——弱网下请求失败被静默吞掉、后端没有幂等、写入没有补偿,三条缺一条就会丢。本文复盘一次 3000 人同时在线的考试报名事故:表单提交丢失率 3.2%,一天漏了 96 条报名;用"前端本地缓存重试 + 后端幂等流水表 + 定时补偿"三件套修完后,到达率稳定在 99.8%。下面把三种方案和代码完整拆开。
客户的活动报名小程序跑在一套托管式建站的 SaaS 底座上,报名表单页面和基础数据由底座承载,日常没出过问题;这次提交丢数据,问题出在我们自己实现的前端提交链路和后端入库逻辑上,和底座本身无关。排查和修复过程完整复盘如下。
一次表单提交的完整链路:用户点提交 → 前端发请求 → 后端写库 → 前端提示成功。这四步里任何一步出问题,用户看到的结果都一样——"没提交成功"或"提交了但没记录"。常见丢法有三种:
定位方法也简单:给前端请求加全局 fail 埋点,给后端入库加流水日志,两边一对就知道丢在哪一段。前端埋点看有没有发出请求、失败了几次;后端流水看收到几条、写库成功几条。这次排查就是靠这个办法定位的:前端日志显示 3.2% 的请求进了 fail 回调,后端流水显示只有 96 条缺了对应记录——数据丢在前端重试环节,而不是后端。
前端是第一个丢数据的口子。弱网下 wx.request 的 fail 回调触发后,很多实现直接不管,用户看到的就是"提交没反应"或"提交成功但后台没有"。
// 提交前先写本地队列,失败自动重试,成功再移除
const pending = wx.getStorageSync('form_pending') || [];
function submitForm(form) {
pending.push({ ...form, status: 'pending', ts: Date.now() });
wx.setStorageSync('form_pending', pending);
flushPending();
}
function flushPending() {
const list = wx.getStorageSync('form_pending') || [];
if (!list.length) return;
wx.request({
url: 'https://api.example.com/form',
method: 'POST',
data: { list },
success: (res) => {
if (res.statusCode === 200) {
wx.setStorageSync('form_pending', []);
}
},
fail: () => {
// 下次启动或网络恢复后再试
setTimeout(flushPending, 30000);
},
});
}关键点:本地缓存是保险,不是主路径。它只解决"前端丢",解决不了"后端重复"——所以必须有方案二兜底。
前端重试之后,后端可能收到同一笔提交多次。后端必须幂等:用业务唯一键(如 用户ID+活动ID+提交时间戳)去重,先查流水再写数据。
def submit_form(form):
biz_key = f"{form['user_id']}:{form['activity_id']}:{form['ts']}"
# 幂等:流水表里已有同 key,直接返回成功,不重复写
if redis.exists(biz_key):
return {'code': 0, 'msg': 'already_submitted'}
# 两阶段:先记流水(状态=进行中),再写业务数据
log_id = insert_form_log(biz_key, 'processing')
try:
insert_form_data(form) # 真正的业务写入
update_log_status(log_id, 'done')
redis.setex(biz_key, 3600, '1')
return {'code': 0}
except Exception:
update_log_status(log_id, 'failed')
raise-- 流水表:幂等键唯一索引 + 状态字段,供定时补偿扫描
CREATE TABLE form_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
biz_key VARCHAR(128) NOT NULL,
status VARCHAR(16) NOT NULL, -- processing / done / failed
payload TEXT,
created_at DATETIME NOT NULL,
UNIQUE KEY uk_biz (biz_key)
) ENGINE=InnoDB;幂等键设计是这节的核心:必须落在"用户+活动+提交时刻"这种业务唯一维度上,用时间戳当 key 的裸做法在弱网重试下照样重复。
两阶段里"先记流水、再写业务"会有中间态:流水是 processing、业务数据还没写进去,进程就崩了。靠用户重试永远补不齐,必须定时扫补偿。
# 每 5 分钟扫一次 processing 超过 60 秒的流水,重新执行业务写入
def compensate():
stale = select_stale_logs(status='processing', older_than=60)
for log in stale:
try:
insert_form_data(json.loads(log['payload']))
update_log_status(log['id'], 'done')
except Exception:
update_log_status(log['id'], 'failed') # 连续失败进告警,人工介入三件套合起来才完整:前端重试保证"不丢",后端幂等保证"不重",定时补偿保证"不卡中间态"。少任何一件,可靠性都打了折扣。这套组合不挑语言和框架,Node、Python、Java 后端都能照这个结构落地,前端换成 uni-app 或原生小程序也只是把 wx.request 换成对应封装。
三件套是完整形态,但不是所有场景都值得全上,按业务价值分级:
判断标准就一条:这条数据丢了,用户会不会投诉。会,就按最高标准做;不会,做轻量版,省下的开发量留给真正重要的链路。下面五个坑,全是实战里真实踩过的。
表单提交可靠性是个系统工程:前端本地缓存重试解决"丢",后端幂等流水解决"重",定时补偿解决"卡"。这次事故从发现到修完用了一天,丢的 96 条报名靠流水表 + 用户反馈手动补齐;三件套上线后,后续两场活动(累计 6000 人次)提交到达率 99.8%,一次漏单都没有。表单是最基础的能力,也是最容易被忽视的可靠性盲区——先把这三件套焊上,比什么都强。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。