自定义设计

那封改变一切的邮件 💬

那天早上,我像往常一样打开邮箱,准备处理用户的反馈邮件。大多是些”链接打不开”、“访问速度慢”之类的问题,我已经习惯了。

但有一封邮件让我停下了手头的动作。

发件人:Nike Digital Team 主题:关于您的短链接服务

您好,

我们在社交媒体上看到很多用户在使用您的短链接服务,效果很好。

我们有一个商业需求:我们希望获得 short.url/nike 这样的品牌短链接。 我们的产品链接会定期更新,但短链接可以保持不变,这样在广告投放时更加稳定。

关于价格,我们可以接受按月或按年付费。请告知您的合作方式。

期待您的回复。

—— Nike Digital Team

我的心跳突然加速了。

付费? 这是我的短链接服务第一次有人主动提出要付费!

我立刻意识到:这不是一个简单的功能需求,这是一个商业机会。

自定义短链接——这三个字在我脑海中闪烁。用户想要自己定义的短链接后缀,而不是系统生成的随机字符。

这不仅仅是个技术问题,这是个产品设计问题。我需要设计一套完整的自定义别名系统。


定义规则:什么别名是合法的?📋

我开始思考第一个问题:什么样的自定义别名应该被允许?

如果允许用户输入任意字符,可能会出现安全问题、URL冲突问题、甚至是系统路径混乱的问题。

我拿出一张纸,开始画规则边界:

┌─────────────────────────────────────────────┐
│           自定义别名验证规则                  │
├─────────────────────────────────────────────┤
│ 1. 长度限制:3-30个字符                       │
│ 2. 字符集:字母、数字、连字符、下划线          │
│ 3. 保留词:系统保留的敏感词汇                  │
│ 4. 重复检查:不能与现有别名冲突               │
│ 5. 敏感词过滤:不当词汇拦截                   │
└─────────────────────────────────────────────┘

规则1:长度限制

为什么不能太短?

长度 < 3字符:
- 组合太少(26个字母 × 26个字母 = 676种)
- 很容易冲突
- 不够有意义
- 容易与系统路径混淆

为什么不能太长?

长度 > 30字符:
- 失去短链接的意义
- 用户体验差
- 违背了"短"的初衷
- 可能超过某些系统的URL长度限制

我先把验证规则写清楚:

落地思路

  • 长度必须在 3 到 30 个字符之间,落在区间外直接拒绝。
  • 少于 3 个字符的组合太少,撞车概率高,也容易和系统路径混淆。
  • 超过 30 个字符就失去了短链接的意义,还会撞上部分系统的 URL 长度上限。

规则2:字符集限制

允许哪些字符?

我查阅了RFC 3986(URL规范),发现URL路径段中安全使用的字符包括:

  • 字母:a-z, A-Z
  • 数字:0-9
  • 连字符:-
  • 下划线:_
  • 点号:.(但我不想使用,因为看起来像文件扩展名)

不允许哪些字符?

× 空格:会被编码成 %20,破坏短链接的简洁性
× 中文/日文等非ASCII字符:需要编码,变长
× 特殊字符:/、?、&、#、@等都有URL特殊含义
× 控制字符:可能破坏系统
× 表情符号:编码后极长

我继续把字符规则写清楚:

落地思路

  • 只放行字母、数字、连字符和下划线,其余字符一律拒绝。
  • 斜杠、问号、井号等在 URL 里有结构含义,放进来会改变路径的解析结果。
  • 空格和中文会被百分号编码,长度膨胀且各平台显示不一致,所以提前拒绝而不是编码后凑合。
  • 点号也一并排除,避免看起来像一个文件扩展名。

规则3:保留词检查

这是最棘手的部分。我需要确保用户不能注册与系统路径冲突的别名。

我列出了第一版保留词清单:

落地思路

  • 保留词存 reserved_words 表,注册时拿别名去比对,命中就拒绝。
  • 清单里至少覆盖系统自身的路径前缀和保留的短码段,防止用户注册出和内部接口撞车的别名。
  • 保留词要做大小写不敏感的比对,否则 Admin 会绕过 admin 这条规则。
  • 保留词命中和敏感词命中的错误提示要分开,否则等于告诉用户哪些词是被系统占用的。

规则4:重复检查

即使别名通过了所有验证规则,我还必须确保它没有被其他用户使用。

落地思路

  • 重复检查查的是 custom_aliases 表,别名唯一性由数据库唯一索引保证。
  • 先查后插只是给用户更快报错;真正防并发重复的是插入时唯一索引拦下的那一下。
  • 查询要走 idx_custom_alias,否则别名数量涨上来后每次注册都要扫全表。
  • 查不到不代表可注册,最终结论以写入结果为准。

规则5:敏感词过滤

最后一个安全网:防止用户注册不当的别名。

落地思路

  • 敏感词过滤放在重复检查之前,先把明显不当的别名挡在写入之前。
  • 词库独立于保留词维护,命中即拒绝,不给「换个说法再试」的机会。
  • 过滤在应用层做,不依赖数据库——敏感词库里没有索引可依赖。
  • 命中的别名要记审计记录,便于事后追溯是谁在什么时候尝试注册了什么。

完整的验证器 🔍

现在我把所有规则整合成一个完整的验证器:

落地思路

  • 验证器按固定顺序逐条判定:长度 → 字符集 → 保留词 → 敏感词 → 重复 → 冲突。
  • 任何一条不通过就立刻返回对应原因,不继续往下查,也不在错误信息里暴露后续规则。
  • 全部通过后才进入写入,写入失败按数据库返回的冲突原因区分「已被占用」和「服务异常」。
  • 验证器是纯函数:输入别名和上下文,输出通过与否加原因,方便单独测试。

数据库设计:自定义别名需要独立存储 💾

我意识到自定义别名和自动生成的短码是两个不同的体系:

自动生成的短码:
- 长度固定(如6字符)
- 字符集固定(Base62)
- 无特殊含义
- 冲突概率极低

自定义别名:
- 长度不固定(3-30字符)
- 字符集宽松但有限制
- 有商业价值
- 冲突概率较高

这意味着我需要设计一个新的表结构,而不是简单地复用现有的urls表。

设计方案1:扩展现有表

数据设计要点

  • 两个方案都要能按别名做唯一性查询,所以索引 idx_custom_alias 在两种设计里都必须存在。
  • 差别在唯一约束挂在哪张表上:urls 上一条记录只能有一个别名,custom_aliases 则是一行一个别名。

优点:

  • 改动最小
  • 查询简单(一张表)

缺点:

  • 字段冗余(short_code和custom_alias)
  • 不够清晰(自定义和自动生成的混在一起)

设计方案2:独立表(我选择这个)

数据设计要点

  • 核心是在 custom_aliases 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 写入时要保证短码唯一、字段完整,并为后续查询留下必要状态。
  • 更新操作通常意味着链接状态变化,要同步考虑缓存刷新和审计记录。
  • 关键字段包括 id、user_id、custom_alias、long_url、created_at、updated_at、word、reason,它们决定了后续查询和管理能力。

优点:

  • 职责清晰
  • 易于扩展(比如未来要给自定义别名加更多属性)
  • 便于审计和历史追踪

缺点:

  • 查询时需要JOIN两张表(但这个场景很少)

我选择了方案2。


冲突处理:两个人都想要 ‘nike’ 怎么办?⚡

这是一个真实的问题:如果有两个商家都想注册 nike 这个别名,应该给谁?

我设计了一个”先到先得”的机制,配合事务保证原子性:

落地思路

  • 规则是先到先得:别名归先注册成功的那个用户,后来的直接收到「已被占用」。
  • 写入放在一个事务里:插入 custom_aliases 并绑定 user_id,要么都成功,要么都回滚。
  • 唯一索引是唯一的裁判:并发下两个事务抢同一个别名,数据库只让一个插入成功,另一个拿到冲突错误。
  • 冲突属于可预期错误,转成用户能看懂的提示,不记为系统故障。

并发安全

在高并发场景下,两个用户可能同时注册同一个别名。我需要确保只有一个人能成功。

落地思路

  • 应用层「先查后插」在并发下会同时看到「不存在」,两个请求都会认为自己能注册成功。
  • 真正兜住并发的是唯一索引:插入时才发现的冲突,比插入前查到的结果更可信。
  • 所以校验分两层:查一次是为了快速反馈,最终以插入是否成功为准。
  • 插入失败时不能留下半条记录——别名和归属必须在同一个事务里提交。

用户体验:实时可用性检查 🎨

我花了两天时间优化一个细节:当用户输入自定义别名时,实时显示它是否可用。

这需要前端的防抖(debounce)和后端的高效验证配合。

前端实现

落地思路

  • 输入框做防抖:停止输入一小段时间后才发请求,而不是每敲一个字符就查一次。
  • 请求带上当前别名和所属链接的上下文,后端返回「可用 / 已占用 / 不合法」三种明确状态。
  • 已占用的别名在输入框旁直接标注,让用户换一个,而不是提交表单后才被告知。
  • 前端的判断只用于提示,最终能不能注册仍以后端写入结果为准。

后端API

落地思路

  • 接口是幂等的:同一个别名重复请求可用性检查,返回结果必须一致。
  • 检查只读不写,命中重复时直接返回占用状态,不触发任何写操作。
  • 这个接口会被公开表单调用,必须限流,否则等于开放了一个别名枚举接口。
  • 返回里不要带上别名的归属者信息,只回答「能不能用」。

这个交互细节花了两天时间,但效果很好——用户可以立即知道他们想要的别名是否可用,避免了填写完表单后才发现别名被占用的挫败感。


上线第一天:有人注册了50个品牌名 🚨

自定义别名功能上线了。

我满怀期待地打开后台监控,看看有没有人会使用这个新功能。

第一天结束时,我看到了一些数据:

自定义别名注册数量:127个
最热门的别名:
- nike (1次)
- adidas (1次)
- apple (1次)
- samsung (1次)
- test (3次)
- mylink (5次)
- brand123 (2次)

看起来还好。

但当我深入查看时,发现了一个问题:有一个用户注册了50个知名品牌的别名。

用户 ID: 18472
注册的别名:
- nike
- adidas
- puma
- reebok
- newbalance
- converse
- asics
- saucony
- ...
(共50个运动品牌名)

更糟糕的是,这些别名指向的URL都是一些不知名的电商网站,很明显是抢注行为。

我意识到:这需要防抢注机制。

问题:

  1. 恶意用户可以批量注册大量品牌名
  2. 真正的品牌所有者反而无法使用自己的品牌名
  3. 这可能导致法律纠纷
  4. 破坏了平台的公信力

我需要解决:

  • 限制每个用户的注册数量
  • 实施品牌名验证机制
  • 建立申诉流程
  • 考虑引入付费门槛

这些,将是下一节的内容。


本节小结 📝

完整的别名验证流程

用户输入别名
    ↓
【规则1】长度检查(3-30字符)
    ↓
【规则2】字符检查(字母、数字、-、_)
    ↓
【规则3】保留词检查(系统路径)
    ↓
【规则4】敏感词检查(不当词汇)
    ↓
【规则5】重复检查(数据库查询)
    ↓
【规则6】冲突处理(事务保证)
    ↓
创建成功 ✅

核心能力拆解

组件职责
CustomAliasValidator完整的别名验证器
AliasCharacterValidator字符规则验证
ReservedWordChecker保留词检查
ProfanityFilter敏感词过滤
custom_aliases 表存储自定义别名
reserved_words 表存储保留词

数据库表结构

数据设计要点

  • custom_aliases 表保存别名本身:归属用户、别名文本、指向的长 URL、创建与更新时间。
  • word 和 reason 记录这类别名为什么被拦下(命中哪条规则),方便申诉时回溯。

想一想

问题1:品牌验证

Nike团队真的注册了nike这个别名,还是被抢注者抢占了?

提示

考虑:

  1. 如何验证用户真的是品牌所有者?
  2. 是否需要上传商标证书?
  3. 是否需要企业认证?
  4. 未认证用户是否允许注册品牌名?

我们将在下一节讨论这个问题。

问题2:批量注册防护

如何防止一个用户在短时间内注册大量别名?

提示

考虑:

  1. 限制每个用户的注册数量(如10个)
  2. 限制注册频率(如每小时最多5个)
  3. 短别名(3-4字符)需要额外审核
  4. 引入付费门槛(减少恶意注册)

问题3:历史遗留

如果某个别名被抢注了,真正的品牌所有者怎么办?

提示

考虑:

  1. 是否可以强制转移别名?
  2. 是否需要建立申诉流程?
  3. 如何证明所有权?
  4. 是否需要仲裁机制?

(下一节:抢注防护与品牌验证)

发现内容有问题?

告诉我们哪里需要改进。