需求与架构设计

写生产代码的第一步不是敲键盘,而是把需求和边界写清楚。本章定义短链接服务的完整设计。

需求定义

编号需求说明
R1用户注册、登录密码哈希存储;登录后签发 JWT(24 小时有效)
R2创建短链接需登录;系统生成 7 位短码,或用户指定自定义短码(全局唯一)
R3短链跳转公开访问;302 临时重定向;不存在的短码返回 404
R4限流跳转接口按来源 IP 限流,防止刷量
R5可观测JSON 请求日志 + Prometheus 指标端点
R6交付docker compose up 一键启动应用与数据库
Tip

明确不做的(防止范围蔓延):不实现点击量统计报表(仅保留计数列)、不支持短码过期、不做管理后台。真实项目中"不做什么"与"做什么"同样重要。

API 设计

方法路径认证请求体成功响应
POST/api/register{"username","password"}201 + {"id","username"}
POST/api/login{"username","password"}200 + {"token"}
POST/api/linksJWT{"url","code?"}201 + {"code","url"}
GET/{code}-302 跳转 / 404

设计要点:

  • 认证走 Authorization: Bearer <token>,与状态码语义一致:401 未认证、403 已认证但越权。
  • 跳转用 302 而非 301:301 会被浏览器永久缓存,短码改指向、点击统计都会失效;302 每次回源,代价可接受。
  • 错误响应统一为 {"error":"..."},便于客户端统一处理。

数据模型

两个实体,GORM 模型定义:

package store

import "time"

type User struct {
    ID           int64     `gorm:"primaryKey" json:"id"`
    Username     string    `gorm:"uniqueIndex;size:32" json:"username"`
    PasswordHash string    `gorm:"size:255" json:"-"` // 永不外泄
    CreatedAt    time.Time `json:"created_at"`
}

type Link struct {
    ID        int64     `gorm:"primaryKey" json:"id"`
    Code      string    `gorm:"uniqueIndex;size:16" json:"code"` // 短码
    URL       string    `gorm:"size:2048" json:"url"`
    UserID    int64     `gorm:"index" json:"user_id"`
    Clicks    int64     `json:"clicks"`
    CreatedAt time.Time `json:"created_at"`
}

分层架构

自上而下单向依赖,层间以接口衔接(可替换、可测试):

cmd/shortener/main.go          装配:配置 → 依赖注入 → 启动

internal/handler               HTTP 层:路由、参数校验、状态码
        │ 依赖接口
internal/service               业务层:短码生成、唯一性、登录逻辑
        │ 依赖接口
internal/store                 存储层:Store 接口 + GORM 实现

internal/auth / ratelimit      横切组件:JWT、令牌桶
Tip

为什么按层而非按"文件类型"组织:internal 下的每个包对应一个明确职责,接口定义在消费方(service 定义自己需要的 Store 接口,store 包实现它),这正是接口一章"小接口定义在使用侧"原则的落地。

目录结构

shortener/
├── cmd/shortener/main.go      # 入口:只做装配
├── internal/
│   ├── handler/               # handler.go、middleware.go、respond.go
│   ├── service/               # link.go、user.go、slug.go
│   ├── store/                 # model.go、gorm.go、errors.go
│   ├── auth/                  # jwt.go、password.go
│   └── ratelimit/             # limiter.go
├── deployments/
│   ├── Dockerfile
│   └── docker-compose.yml
├── .golangci.yml
├── .github/workflows/ci.yml
├── Makefile
└── go.mod                     # module example.com/shortener

技术选型

选择理由
HTTP标准库 net/http(Go 1.22 路由)教学透明,无额外依赖
存储PostgreSQL + GORM数据库章节一致
认证golang-jwt/jwt/v5 + x/crypto/bcrypt两者的事实标准
限流golang.org/x/time/rate标准扩展库,令牌桶
配置环境变量12-Factor,容器友好
日志/指标slog + Prometheus可观测性章节一致

短码生成策略

系统短码使用 Base62 编码 + crypto/rand 随机 7 位(62⁷ ≈ 3.5 万亿组合)。关键设计:

  • 用密码学随机而非时间戳递增,避免短码可被枚举遍历(gosec 也会拦截弱随机)。
  • 冲突处理:生成后插入,唯一索引冲突时重试(3 次上限);3.5 万亿空间下冲突概率可忽略。
  • 自定义短码仅允许 [0-9a-zA-Z_-]{3,16},正则校验防注入与畸形输入。

下一章进入核心实现。