第一个用户

发布两周了,零个用户

我花了两个周末写完短链接服务,又花了一个周末部署上线。然后在 V2EX、掘金、少数派上各发了一篇介绍帖。

三天过去,访问量 47。注册用户:0。

一周过去,访问量 112。注册用户:还是 0。

说实话,我有点沮丧。每天打开服务器监控面板,看到的只有我自己测试时留下的请求。那些 /a3f8c2/b7d9e1 的跳转记录,全是我自己的 IP。

第二周结束时,我认真考虑要不要把这台每月 ¥68 的云服务器退掉。反正也没人用,省点钱不好吗?

那天晚上,我正准备登录云服务商控制台,Gmail 弹出了一封新邮件。

发件人:王志远(小王)

我的心跳快了半拍。


第一封邮件

嗨,我是小王,一个独立开发者。

我刚刚发布了自己的第一个 SaaS 产品——一个在线简历生成工具。需要在社交媒体上推广,但产品链接实在太长了:

https://www.myresumebuilder.com/landing?utm_source=twitter&utm_medium=social&utm_campaign=launch

我在 V2EX 上看到你的短链接服务,想试试!请问怎么使用?

另外说一句,你的服务真的很快,页面秒开。

我激动得差点从椅子上跳起来。不是因为它是一封”用户反馈”——而是因为真的有人,一个活生生的人,在互联网的某个角落,用了我写的东西。

我花了五分钟冷静下来,然后认真写了一封回信,附上了完整的 API 调用示例。


第一次真实访问

十分钟后,我的服务器日志开始滚动了。

小王按照我给的文档,发出了他的第一个请求:

验证要点

  • 命令只用于验证系统状态,读者不需要记具体参数。
  • 请求验证关注返回状态、跳转目标和响应时间。

服务器记录了全过程:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

返回结果:

配置要点

  • 配置表达的是环境差异和运行参数,不是业务规则本身。

然后我盯着终端等了整整十分钟。终于在 10:33,日志里出现了第一行来自外部用户的访问记录:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

302 跳转,成功了。

那一刻我才真正理解了一件事:**服务在自己电脑上跑通是一回事,让别人在真实世界里用上是另一回事。**那种感觉完全不同。有人在 Twitter 上点了 short.url/a3f8c2,然后在浏览器里看到了他自己的产品页面。我的服务,完成了这次连接。


用户反馈:原来不只是缩短链接

一周后,小王发来了一封很长的反馈邮件:

太棒了!短链接服务很好用!

我在 Twitter、Facebook、LinkedIn 都分享了短链接,效果很好。

不过,我有几个建议:

  1. 点击统计:我想知道我的短链接被点击了多少次——哪个渠道来的多,哪个渠道没人点。这对营销来说太重要了。
  2. 自定义短链接:我想要一个更好记的,比如 short.url/myresume,可以印在名片上。
  3. 有效期设置:有些推广活动只搞一周,链接能不能自动过期?
  4. API 文档:最好有更详细的 API 文档和接入说明,我打算集成到我的产品里。

我读完邮件,靠在椅背上想了很久。

原来用户需要的不只是”缩短链接”这么简单。他们要的是可追踪、可管理、可集成的链接服务。缩短只是最基础的功能——就像搜索引擎的搜索框只有一个,但背后有排名、有推荐、有广告。

我打开一个空白文档,列出了新的开发计划。但在此之前,我需要先处理另一个更紧迫的事情。


第一个 Bug

就在我觉得一切顺利的时候,小王发来了一条消息:

你好,我发现了一个问题:

我创建的短链接 https://short.url/d7f3b2 在某些浏览器中无法跳转。

具体表现:

  • Chrome:正常跳转 ✅
  • Firefox:正常跳转 ✅
  • Safari:无法跳转 ❌
  • 微信内置浏览器:无法跳转 ❌

能否帮忙看看?

收到这条消息的时候,我的心凉了半截。我只有一个用户,一个!而且他已经遇到了 Bug。

我马上打开服务器开始排查。

问题定位

首先检查重定向的代码逻辑——看起来没什么问题:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

然后我去查不同浏览器的访问日志:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

Safari 和微信都返回了 404?但 Chrome 和 Firefox 没问题?

我盯着这几行日志看了半天,突然一个念头闪过。

根因:大小写敏感

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

原来 Safari 和微信内置浏览器在某些场景下会把 URL 路径转为大写(或保持用户输入的原始大小写)。而我的数据库查询用的是 SQLite,默认是大小写敏感的。WHERE short_code = 'D7F3B2' 当然找不到 d7f3b2

找到了!就是它。

修复方案

我列了三个方案:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

方案 1 和方案 2 都是在应用层做转换,简单但容易遗漏——万一哪个新接口忘了加 .lower() 就又出 Bug。

我选择了方案 3:数据库层面解决,一次性堵死:

数据设计要点

  • 核心是在 urls 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 关键字段包括 idshort_codelong_urlcreated_at,它们决定了后续查询和管理能力。

COLLATE NOCASE 让 SQLite 在比较 short_code 时自动忽略大小写。存储时保留原始大小写,查询时大小写不敏感,索引也能正常使用。一劳永逸。

我改完表结构,重新部署,给小王回了一封邮件:“已修复,请再试试。”

五分钟后他回复:“Safari 和微信都正常了!”

我长舒一口气。


系统改进

Bug 修完了,但这次事故让我意识到:我的系统太简陋了。一个大小写问题就让我手忙脚乱。如果以后用户多了,还不知道会出什么幺蛾子。

我决定在加新功能之前,先把系统架构升级一下。

当前系统的现状

先看看现在的处理链路有多朴素:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

没有缓存,没有异步处理,没有统计。每次重定向都直接查数据库。

升级方案:缓存 + 异步日志

我引入了两项关键改进:Redis 缓存异步日志队列

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

为什么是异步日志?因为每次重定向都要写数据库的话,一旦数据库变慢,用户等的就是跳转——那是体感最明显的延迟。把日志扔进队列,后台线程慢慢写,用户完全无感知。

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

改进后的数据库表结构也做了调整:

数据设计要点

  • 核心是在 urls 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 索引服务于高频查询,重点关注 AUTOINCREMENTidx_short_codeidx_clicked_at
  • 关键字段包括 idshort_codelong_urlcreated_atclicked_atipuser_agentreferer,它们决定了后续查询和管理能力。

用户使用场景观察

上线后我观察了几天,发现用户的使用方式比我预想的丰富得多:

  • 社交媒体营销:像小王这样的独立开发者,在 Twitter、LinkedIn 分享产品链接,链接被大量点击,需要稳定性。
  • 短信营销:每条短信按字符计费,链接越短越省钱,5-6 个字符的短码是刚需。
  • 二维码生成:线下推广场景,短链接生成的二维码更简单,更容易扫描。
  • API 集成:有些开发者想把短链接功能嵌入自己的产品,需要 API 和文档。

第一周的数据

一周后,我统计了一下数据:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

23 个独立用户,1243 次重定向,0 错误,99.9% 可用性。

成本:¥68/月(云服务器)+ ¥0(Redis 还跑在同一台机器上)。

收入:¥0。

但至少有人在用了。而且他们似乎还愿意继续用下去。