自建短链接服务。把长 URL 变成 https://short.zzheng.dev/<6位短码>,供自己日常分享使用。
| 组成 | 位置 | 说明 |
|---|---|---|
| 后端服务 | 仓库根目录(Go + Gin) | 提供创建短链与跳转的 HTTP 接口 |
| Raycast 客户端 | raycast-extension/(TypeScript) |
日常使用入口:在 Mac 上一条命令生成短链 |
在 MacBook 上用 Raycast 命令 Short URL Generator:
- 打开 Raycast,输入
Short URL Generator - 在搜索框粘贴或输入要缩短的 URL
- 回车 → 提示「短链接已生成」,短链已自动复制到剪贴板,直接粘贴即可分享
输入要求与容错:
- 接受
http/https地址;不写协议会自动补https://,所以直接粘贴example.com/path也能用 - 只补协议头,不会改写你给的路径与查询串
- 非法输入(例如
qwq、http:///Volumes/xxx这类主机名不成立的内容)会被拦下并提示原因,不会提交
这是本地开发的扩展,没有发布到 Raycast Store。它由 MacBook 上的 launchd 常驻
ray develop维持注册; 安装、验证与排障见 Ops 仓库的hosts/macbook/raycast-shorturl/。
curl -X POST https://short.zzheng.dev/shorten \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/some/very/long/path?with=query"}'返回:
{
"code": 200,
"msg": "Short URL created successfully",
"data": {
"id": 2098322940138360832,
"short_url": "https://short.zzheng.dev/aB3xY9"
}
}拿到的 short_url 直接访问即可。
浏览器或 curl 访问 https://short.zzheng.dev/<短码>,服务返回 302 跳转到原始地址:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://short.zzheng.dev/aB3xY9
# 302 https://example.com/some/very/long/path?with=query| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/shorten |
创建短链。body {"url":"..."} |
GET |
/<短码> |
302 跳转到原始地址 |
GET |
/ping |
存活探针,返回 pong |
错误响应统一是 {"code":<非200>, "msg":"..."}:
| HTTP | code |
场景 |
|---|---|---|
| 400 | 400 | body 不是合法 JSON,或缺少 url |
| 404 | 404 | 短码不存在,返回 Short URL not found |
| 500 | 500 | 写库失败 |
实现细节(需要时再看):
- 短码是 6 位 base62 随机串(
crypto/rand),不是自增 ping/shorten/info是保留字,不会被分配为短码- 同一个 URL 重复提交会生成多条记录(没有按 URL 去重)
后端跑在家里 NAS 上,NAS 没有公网 IP,所以中间要经过两步"借道"才能被公网访问:
① Raycast 插件(MacBook) ② curl / 浏览器(任意设备)
│ HTTPS POST /shorten │ HTTPS GET /<短码>
└──────────────┬───────────────────────┘
▼
③ 阿里云 Nginx(120.55.127.195:443)
· TLS 终止,持有 short.zzheng.dev 证书
· 把请求转给本机 FRPS(只暴露 80/443,NAS 不直接对外)
▼
④ FRPS(阿里云)⇄ FRPC(NAS,代理名 logit-shorturl)
· 反向隧道:把公网流量"穿"进没有公网 IP 的 NAS
▼
⑤ NAS Docker Compose 项目 short-url
├── short-url 业务容器 :8087 ← 真正处理请求
└── short-url-redis 缓存容器 :6379(仅容器内部通信)
▼
⑥ SQLite 文件 /home/ziheng/short-url-data/shorturls.db ← 短链存这里
各段为什么存在:
| 段 | 组件 | 作用 | 少了它会怎样 |
|---|---|---|---|
| ③ | 阿里云 Nginx | 固定公网入口 + HTTPS 证书 | 没有公网域名,只能用 IP+端口;也没有 TLS |
| ④ | FRPS / FRPC | 内网穿透 | NAS 无公网 IP,外界直接访问不到 |
| ⑤ | Docker Compose | 托管业务容器与缓存容器 | 手工 docker run 容易漂移(曾因此出过问题) |
| ⑥ | SQLite | 持久化短链 | 重启后数据丢失 |
创建短链(POST /shorten):
写 SQLite(新短链记录)
└─ 同时写 Redis 缓存(key = 短码,TTL 24 小时)
访问短链(GET /<短码>):
先查 Redis ──命中──▶ 直接 302
│
未命中
▼
查 SQLite ──找到──▶ 302,并异步把 visit_count / last_visit 落库
│
没找到
▼
404
Redis 只是读缓存,且是可选的:连不上只会打一行错误日志,不影响创建与跳转(服务本身仍能正常工作,只是每次都查库)。
.
├── README.md 本文件:用法与连接关系
├── Makefile 构建 / 运行 / 部署 / 扩展的统一入口(make help 看全部)
├── docs/deployment.md 部署、验证、回滚手册
├── main.go 后端入口与路由
├── config/ 配置加载(viper,支持环境变量覆盖)
├── database/ SQLite(GORM AutoMigrate)与 Redis 初始化
├── handlers/ HTTP 处理器:/shorten、/<短码>
├── models/ 数据模型(短链、会话、访问记录、地理位置)
├── services/ 业务逻辑(创建、缓存读写、访问统计)
├── utils/ 工具(雪花 ID、6 位 base62 短码)
├── tests/ 单元 / 集成 / 演示测试
├── Dockerfile 多阶段构建(CGO + sqlite)
├── docker-compose.yml NAS 部署定义(路径为 NAS 绝对路径)
└── raycast-extension/ macOS Raycast 客户端
后端保持在仓库根目录(而不是挪进 backend/)是刻意选择:Dockerfile、docker-compose.yml 与部署流程都以仓库根为构建上下文,移动目录会连带改动部署链路和 NAS 上的克隆路径,收益不抵风险。
make dev # Air 热重载(需先 go install github.com/air-verse/air@latest)
make build # 编译到 tmp/short-url
make test # go test ./...
make lint # go fmt + go vet不依赖 Docker 直接跑(会用到仓库里的 config.yaml):
go run .
curl -s http://127.0.0.1:3001/ping # config.yaml 默认端口是 3001cd raycast-extension
pnpm install # 包管理器是 pnpm(仓库内为 pnpm-lock.yaml)
pnpm run dev # ray develop:注册到 Raycast,改动热重载
pnpm run lint扩展的输入校验逻辑是纯函数 src/validate-url.ts,可以脱离 Raycast GUI 测试:
make ext-test # 19 条用例(合法 URL、无协议、IP、localhost、各类非法输入)
pnpm run dev只在进程存活期间注册扩展,关终端就消失。长期可用要交给 launchd 常驻。
部署目标只有一处:NAS。镜像在开发机交叉编译为 linux/amd64,导出 tar 传到 NAS 后 docker load。
make deploy-nas做的事:检查工作区干净 → 构建 short-url-nas:<git-short-sha> → docker save → rsync 到 NAS → 写 NAS 部署目录的 .env(IMAGE_TAG)→ docker load + docker compose up -d → 自检 /ping。
三个要点:
- 镜像标签 = 源码 commit SHA,所以"线上跑的是哪个版本"总能对回某个提交。回滚不必重新构建:
make images-nas # 看 NAS 上已有哪些标签 make rollback-nas TAG=<旧 sha> # 改 .env 后 up -d
- 部署不会覆盖线上配置。运行配置在 NAS 上独立维护(
/home/ziheng/short-url-config/config.yaml),让"改配置"和"发版本"解耦。改配置走配置,改代码走部署。 - 工作区脏时默认拒绝部署(标签会与构建内容不符,之后无法凭标签回滚)。确需跳过用
ALLOW_DIRTY=1 make deploy-nas。
完整流程、验证清单、回滚与历史残留说明见 docs/deployment.md。
优先级:环境变量 > config.yaml > 代码内默认值。
server:
port: "8087"
gin_mode: "release" # debug | release
domain: "https://short.zzheng.dev/" # 用于拼接返回的 short_url
database:
driver: "sqlite"
dsn: "/data/shorturls.db"
redis:
host: "redis" # 容器内用 compose 服务名;本地调试可写 localhost
port: "6379"
password: ""
db: 0两个容易踩的点:
server.domain必须以/结尾:返回的short_url是domain + 短码直接拼接- 容器内
redis.host要写redis而不是localhost:Redis 是独立容器,写localhost会指向业务容器自身(曾经因此让缓存半年没生效)
环境变量可覆盖(viper AutomaticEnv):SERVER_PORT、SERVER_GIN_MODE、DATABASE_DSN、REDIS_HOST、REDIS_PORT。
| 主题 | 位置 |
|---|---|
| 部署、验证、回滚、历史残留 | docs/deployment.md |
| Raycast 扩展的安装与 launchd 常驻 | Ops 仓库 hosts/macbook/raycast-shorturl/ |
| NAS 侧服务配置副本与漂移记录 | Ops 仓库 hosts/nas/short-url/ |
证书与 ACME 续期(short.zzheng.dev) |
Ops 仓库 docs/services/证书与ACME续期速查.md |
- 没有鉴权:能访问
/shorten就能创建短链。属自用范围的取舍,靠入口未公开 + HTTPS 控制。 - 没有删除接口:清理脏数据只能直接改 SQLite,并同步删掉 Redis 里同名 key,否则缓存还在、短链仍能跳转。
- 不按 URL 去重:同一 URL 反复提交会产生多条短链。
- 访问统计有延迟:重定向只读
original_url,命中缓存时不回写,所以visit_count不保证即时准确。 - 运行镜像不随源码自动更新:改完代码必须显式
make deploy-nas。 - 单点:没有公网冗余,阿里云或 NAS 任一不可用都会导致短链失效。