做企业官网,最怕的不是没流量,是流量来了、客户也填了表单,线索却在半路丢了或者重复。这类问题表面看是"表单不好用",拆开其实是三处:弱网下请求根本没到服务器、请求超时其实已经入库但客户以为没成功又点了一次、以及通知被折叠销售误以为没收到。前端做防重和本地暂存、后端用幂等键去重、再加一道线索对账,三处补齐,漏单和重复基本能清零。
客户是一家做本地企业服务的公司,官网主要承接搜索和线下推广带来的询盘。站点的内容管理和表单后台跑在一套托管式建站的SaaS底座上,日常的页面发布、留言接收和数据入库都在后台正常完成。这篇不聊后台怎么配置,只复盘一次"客户明明点了提交、销售却没收到,偶尔还冒出两条重复线索"的表单故障。问题最后定位在前端弱网交互、网关超时重试和通知对账这几处,下面把排查和改造过程记下来。
销售一口咬定"客户填了但我没收到",先别急着改代码,把数据拉出来对一遍。我们把后台线索导出,和销售自己登记的跟进表逐条比对了两周:
这一步很关键:"没收到通知"不等于"没提交成功"。通知(邮件、短信、企微)和数据入库是两条独立的链路,很多所谓漏单其实是通知链路的问题。先把真假丢单分清楚,后面才知道改哪里。
真正在传输层丢掉的请求,靠用户多戳几次是没用的,反而会制造重复。前端要解决两件事:提交期间锁住入口,以及失败时先把数据存在本地、网络恢复再补发。
// 进入填写页就生成一次幂等令牌,同一份填写反复提交都是同一个键
const formKey = 'lead_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
const pendingKey = 'pending_lead';
const btn = document.querySelector('#submit');
let submitting = false;
async function submitLead(data) {
if (submitting) return; // 防连点
submitting = true;
btn.disabled = true;
const payload = { ...data, idemKey: formKey };
try {
const res = await fetch('/api/lead', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': formKey },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error('http ' + res.status);
localStorage.removeItem(pendingKey); // 只有确认成功才清本地
alert('提交成功,我们会尽快联系您');
} catch (e) {
// 弱网/超时:先落地本地,恢复后补发,绝不让客户白填
localStorage.setItem(pendingKey, JSON.stringify(payload));
scheduleRetry();
} finally {
submitting = false;
btn.disabled = false;
}
}
// 网络恢复或定时轮询时补发;后端靠 idemKey 去重,补发也不会产生两条
function scheduleRetry() {
window.addEventListener('online', retryPending);
setInterval(retryPending, 30000);
}
async function retryPending() {
const raw = localStorage.getItem(pendingKey);
if (!raw || !navigator.onLine) return;
const payload = JSON.parse(raw);
const res = await fetch('/api/lead', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': payload.idemKey },
body: JSON.stringify(payload),
});
if (res.ok) localStorage.removeItem(pendingKey);
}这里有个容易忽略的点:幂等令牌要在开始填写时生成并跟着数据一起存进本地,而不是每次点击都新生成。否则弱网暂存后补发,换了新令牌,后端就认不出是同一条了。
前端按钮锁只能挡住正常操作,挡不住刷新页面、杀进程重进、以及网关重放。同一条线索会不会变成两条,最终由后端说了算。做法是给线索表加唯一约束,再配一张幂等记录表,重放请求直接返回第一次的结果。
-- 线索表:幂等键加唯一约束,同一次提交无论重放几次只落一条
CREATE TABLE customer_lead (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
idem_key VARCHAR(64) NOT NULL COMMENT '前端生成的幂等令牌',
name VARCHAR(50),
phone VARCHAR(20),
content VARCHAR(1000),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_idem_key (idem_key)
);
-- 幂等记录表:先占坑再处理,处理结果可复用
CREATE TABLE idempotent_record (
idem_key VARCHAR(64) PRIMARY KEY,
status TINYINT COMMENT '0处理中 1成功 2失败',
response VARCHAR(1000),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);接收接口的逻辑是:先查有没有处理过,有就直接回第一次的结果并标记重复;没有就先占坑,再写库,写库时即使并发撞上唯一约束,也当作重复返回,而不是报错。
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post('/api/lead')
def create_lead():
idem = request.headers.get('Idempotency-Key') or request.json.get('idemKey')
if not idem:
return jsonify(msg='missing idempotency key'), 400
# 已处理过:重放直接返回首次结果,不再插入
cached = get_idempotent(idem)
if cached and cached['status'] == 1:
return jsonify(data=cached['response'], duplicated=True)
# 占坑失败说明正在并发处理或重放
if not try_insert_idempotent(idem, status=0):
return jsonify(msg='processing, retry later'), 409
try:
lead_id = insert_lead(request.json) # 数据库唯一键是最后一道闸
resp = {'lead_id': lead_id}
mark_idempotent(idem, 1, resp)
return jsonify(data=resp)
except DuplicateKeyError:
return jsonify(data=get_idempotent(idem)['response'], duplicated=True)
except Exception as e:
mark_idempotent(idem, 2, str(e))
raise有一类重复非常隐蔽:客户只点了一次,但反向代理把 POST 转发给后端时超时,网关按默认策略把请求又转发了一遍,后端于是收到两次。GET 重试无所谓,POST 重试就会造数据,必须在网关显式关掉。
location /api/lead {
proxy_pass http://backend;
proxy_next_upstream off; # 失败不切换、不重放 POST
proxy_connect_timeout 5s;
proxy_read_timeout 15s;
# 失败交给前端带着幂等键安全重试,网关自己绝不重放
}
client_body_timeout 15s;传输层补齐后,剩下的就是通知链路。我们做了两件事:一是把通知收件箱加白、关键渠道做双通道(邮件之外再推一条企微/短信),避免进垃圾箱;二是每天早上跑一个对账,比对后台线索数和销售跟进数,出现"入库了但没人跟进"的记录就告警。
改造后连续观察了 6 周:前端本地暂存补发成功 11 条,这 11 条在改造前就是石沉大海的漏单;后端幂等拦截重复提交 9 次;每天对账线索入库数和跟进数 47 比 47 对齐,再没出现过"客户说提交了、销售说没收到"的扯皮。
官网询盘表单的可靠性,说穿了就三层:前端保证弱网不丢、连点不重,后端用幂等键保证同一条数据只落一次,再用对账兜底通知和跟进。对中小企业来说,表单里躺着的不是一条数据,是一条真金白银的销售线索,把这条链路当成交易接口来做防丢防重,比多投一点推广更划算。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。