1. 项目代号是怎么来的:477777背后的需求起点
先交代一下背景。这个项目最初根本没有名字,需求方只丢过来一句话:我们要做一批优惠券,每张券对应一个唯一码,用户输进去就能兑换权益,总量大概几十万到百万级别,要支持批量生成、导出、验证,另外码不能太长,最好用户在手机上也敲得进去。
听起来很简单,对吧?我当时也以为很简单。直到我把第一次测试数据导出来,发现日志刷屏全是477777xxxx,仔细一查,是需求方说"前缀就按你手机尾号来",我随口报了个4777,于是所有测试码都变成了477777开头的样子。后来项目在内部各处引用的时候,大家干脆直接用477777指代这套系统,"477777"就成了正式代号。
从实际经验看,这个代号的来历并不重要,重要的是它把一类非常普遍的需求具象化了:**任何需要"给用户一个不重复的、可以手工输入的唯一标识"的场景,本质都是同一套问题。**优惠码、兑换码、邀请码、活动抽奖码、内部工单号、优惠券批次号,底层逻辑几乎一模一样。
所以我写这篇内容,不只是在讲一个优惠码项目,而是在讲一套可以复用到很多地方的生成、校验、存储、防并发消费的完整方案。如果你之前只用过uuid或者随机字符串随便拼一下就上线,那这篇文章值得你看完,因为里面好几个坑,我都是实际踩过之后才补上的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 码格式设计:为什么前缀、随机段、校验位缺一不可
很多第一次做优惠码的同学,第一步就踩坑:直接random_string(16)生成一串大小写字母加数字丢给用户。结果就是用户收到码以后,分不清0和O、1和l,输入的时候反复报错,客服被打爆。
我在477777项目里一上来就把格式定了三层结构:
| 码段 | 长度 | 说明 |
|---|---|---|
| 前缀 | 4位 | 固定为P477,用于标识业务来源和批次 |
| 随机段 | 8位 | 核心随机部分,决定唯一性 |
| 校验位 | 1位 | 防止用户手输错误,兑换前先本地校验 |
最终码形如:P477-8F3KQ2M1-7,中间加连字符纯粹是为了让人眼分组读取,存数据库的时候去掉连字符统一存储。
2.1 随机段字符集的选择不能偷懒
随机段我用了32字符集:ABCDEFGHJKLMNPQRSTUVWXYZ23456789。去掉I、O、0、1这四个容易混淆的字符。很多人觉得少几个字符无所谓,但实际上字符集大小直接影响碰撞概率,而且设计不好会在后端引发重复码问题。
字符集是32,随机段长度是8,那么理论空间是32^8 ≈ 1.1万亿。即使生成100万条码,占用的空间比例也极小,碰撞概率可以忽略。但如果有人图省事,直接用string.ascii_letters + digits,62字符集空间确实大了很多,可是用户体验极差,用户根本分不清某些字符,反而增加了客服成本。码不是越复杂越好,而是越适合输入越好。
2.2 校验位的计算方式
校验位的用途是:用户手工输入时如果敲错了一个字符,系统可以在不查数据库的情况下直接判断"这个码无效",避免无效请求打到数据库。
我用的校验算法很简单:把随机段的每个字符映射到它在字符集里的位置索引,然后对所有索引值加权求和,取模32得到校验位。这个算法没有标准名叫什么,实现也就几十行:
python复制import secrets
ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
ALPHABET_INDEX = {c: i for i, c in enumerate(ALPHABET)}
def _checksum(code: str) -> str:
total = 0
for i, ch in enumerate(code):
total += (ALPHABET
