首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >导出的报名名单一堆重复和乱码:轻应用表单的服务端校验、归一化与自动去重实践

导出的报名名单一堆重复和乱码:轻应用表单的服务端校验、归一化与自动去重实践

原创
作者头像
用户12754333
发布2026-09-14 14:15:48
发布2026-09-14 14:15:48
90
举报

导读

一次线下活动结束,运营从报名轻应用里导出名单准备发短信,打开表格就懵了:同一个人出现三行,手机号有的带空格、有的带 +86,城市一栏"厦门""厦门市""福建厦门"混着排,还有几条姓名是乱码和"测试测试"。活动来了五百多人,名单清洗花了整整一个下午,还漏掉了几个真用户。问题不在用户不认真,而在我们把表单当成了"填什么存什么"的透传管道。这篇讲轻应用表单数据在服务端怎么做校验、归一化和自动去重,把脏数据挡在入库之前,附上关键代码和五个踩过的坑。

一、先想清楚:脏数据是从哪几个口子漏进来的

很多人第一反应是"前端多加点校验不就行了"。但前端校验只能拦住正常用户的手滑,拦不住三类真实来源:一是用户换了设备、退出去重填,提交了两次;二是弱网下前端以为没提交成功、用户反复点按钮;三是接口被直接调用,绕过页面。所以前端校验是体验,服务端校验才是底线,凡是要落库、要用于后续触达的数据,服务端必须独立校验一遍,不能信任任何来自前端的字段。

我们把问题归成三类,分别对应不同处理手段:格式不合法(乱码、缺位手机号)要拦,格式不统一(空格、大小写、全半角)要归一,指向同一主体的多条记录要去重。下面逐个说。

二、第一道:服务端校验,不合格的当场拦下

校验要按字段类型给出明确规则,而不是一个"不能为空"糊过去。手机号、邮箱用正则+号段判断,必填项判空,枚举字段(城市、意向课程)必须落在允许集合内,防止接口被塞进任意值。

代码语言:javascript
复制
const VALID_CITY = new Set(['厦门', '福州', '泉州', '漳州']);

function validateLead(raw) {
  const errors = [];
  const name = (raw.name || '').trim();
  if (!name || name.length < 2) errors.push('姓名不合法');
  // 去掉所有非数字字符后再判断,兼容空格、横杠、+86
  const phone = (raw.phone || '').replace(/\D/g, '').replace(/^86/, '');
  if (!/^1[3-9]\d{9}$/.test(phone)) errors.push('手机号不合法');
  if (raw.city && !VALID_CITY.has(raw.city)) errors.push('城市不在可选范围');
  return { errors, cleanPhone: phone };
}

校验不通过不要直接 500,要把具体哪个字段、什么原因返回给前端,让用户能改;明显是机器乱填的(比如姓名是连续重复字符、内容里带脚本标签)直接丢弃并记一条安全日志。

三、第二道:归一化,让"同一个东西"长得一样

去重之所以难,是因为同一份信息有无数种写法。归一化的目标是在入库前把每个字段收敛成唯一标准形态,去重才有依据。

代码语言:python
复制
import re

def normalize(lead: dict) -> dict:
    # 手机号:去非数字、去国家码,这是去重主键
    phone = re.sub(r'\D', '', lead.get('phone', ''))
    if phone.startswith('86') and len(phone) == 13:
        phone = phone[2:]
    # 文本:去首尾和中间多余空白、全角转半角、统一大小写
    name = re.sub(r'\s+', '', lead.get('name', ''))
    name = name.translate(str.maketrans('ABCDE', 'ABCDE'))
    # 城市:去掉行政后缀,再映射回标准名
    city = re.sub(r'(市|省|区|县)$', '', lead.get('city', ''))
    alias = {'福建厦门': '厦门', '厦門': '厦门'}
    city = alias.get(city, city)
    # 邮箱统一小写,避免 A@x.com 和 a@x.com 被当成两个人
    email = lead.get('email', '').strip().lower()
    return {'phone': phone, 'name': name, 'city': city, 'email': email}

几个容易漏的点:全角半角、不可见字符(零宽空格)、中英文标点、邮箱大小写、城市/院校这类需要别名映射的字段。归一化逻辑要集中在一个地方维护,别在导出时才临时清洗——那时已经污染了数据库。

四、第三道:自动去重,分"实时拦截"和"批量合并"两层

实时层在提交时就挡:以归一化手机号为唯一键,配合数据库唯一索引兜底,重复提交不再产生新行,而是更新原记录的更新时间和补充字段。

代码语言:sql
复制
-- 手机号唯一索引,是并发下去重的最后一道硬保障
ALTER TABLE lead ADD UNIQUE KEY uk_phone (phone);
代码语言:javascript
复制
async function upsertLead(lead) {
  const n = normalize(lead);
  // 存在则更新补充信息、不存在则插入,靠唯一索引保证并发安全
  await db.query(`
    INSERT INTO lead(phone,name,city,email,created_at,updated_at)
    VALUES(?,?,?,?,NOW(),NOW())
    ON DUPLICATE KEY UPDATE
      name=VALUES(name), city=VALUES(city), updated_at=NOW()
  `, [n.phone, n.name, n.city, n.email]);
}

弱网下用户连点、前端重试,请求可能在毫秒级并发到达,单纯"先查再插"仍会双写,必须靠唯一索引 + ON DUPLICATE KEY UPDATE(或 PostgreSQL 的 ON CONFLICT)在数据库层收口。

批量层处理历史存量:对已经脏了的老名单,按归一化手机号分组,同组保留信息最全的一条为主记录,其余的关键字段(备注、来源、不同时间的报名记录)合并而不是物理删除,避免误删真实信息。

并不是所有场景都能拿到手机号当唯一键,比如有些轻应用只留邮箱或微信。这时单字段不够用,要做"复合指纹":把归一化后的姓名、联系方式、城市等几个稳定字段拼起来做一次哈希,用这个哈希当辅助去重键。它的准确率不如手机号,同名同城的人可能被误判,所以复合指纹只用于"提示疑似重复、进入待人工确认队列",不自动合并。我们踩过的教训是:一开始让复合指纹自动合并,结果把两个同名同姓、来自同一家公司的不同学员合成了一条,对方少收了一份上课通知。去重的自动化程度要和字段的唯一可靠程度匹配,强标识(手机号、证件号)可以自动收口,弱标识只做提示,这条边界要守住。

五、踩坑清单

  • 坑1:只做前端校验。接口可被直接调用、弱网重试也绕过页面,服务端必须独立校验,前端只负责体验。
  • 坑2:拿原始输入直接去重。带空格、+86、全角字符的手机号永远去不掉,必须先归一化再比对,归一化是去重的前提。
  • 坑3:先 SELECT 查有没有、再 INSERT。高并发下两个请求同时查空就双双插入,去重要落到唯一索引和 upsert,不能只靠应用层判断。
  • 坑4:去重时物理删除"重复行"。重复记录里可能带着不同时间的报名轨迹和备注,应合并保留、标记主记录,而不是直接删。
  • 坑5:归一化规则散落在各处。导出脚本一套、入库一套,结果越清越乱,归一化逻辑要单点维护、入库时统一执行。

结语

表单不是透传管道,而是数据质量的第一道闸门。记住"校验拦非法、归一化统形态、唯一键加 upsert 去重"这三层,顺序不能反:不校验,脏数据进得来;不归一化,同一个人认不出来;不靠数据库唯一索引,并发下照样重复。把这三步做在入库之前,运营下次导出名单,拿到的就是一份能直接用的干净数据,而不是一下午的手工活。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 导读
  • 一、先想清楚:脏数据是从哪几个口子漏进来的
  • 二、第一道:服务端校验,不合格的当场拦下
  • 三、第二道:归一化,让"同一个东西"长得一样
  • 四、第三道:自动去重,分"实时拦截"和"批量合并"两层
  • 五、踩坑清单
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档