Dynamic QR Code - 用短網址讓 QR Code 可以更新
前言
build-moat-live-sessions/qr_code_generator 是一個 Dynamic QR Code prototype。
它和一般 QR Code 最大的不同是:QR Code 圖片裡不直接放原始 URL,而是放一個短網址。
也就是說,使用者掃到的是:
http://localhost:8000/r/Os8TXDb
伺服器再把這個短網址 redirect 到真正目標:
https://example.com
這樣設計後,QR Code 圖片不用重新產生,後台就可以修改目標網址,也能記錄掃描次數。
Static QR Code 的限制
傳統 static QR Code 是把資料直接寫死在圖片裡。
例如把 https://example.com/product-a 編碼進 QR Code 後,使用者掃描就直接得到這個網址。
問題是:
- 目標網址改了,QR Code 圖片就失效。
- 印刷品已經發出去,無法回收。
- 無法知道有多少人掃描。
- 無法做過期、停用或 A/B test。
Dynamic QR Code 的核心就是多加一層 server redirect。
API 設計
專案提供的主要 endpoint:
POST /api/qr/create
GET /r/{token}
GET /api/qr/{token}
PATCH /api/qr/{token}
DELETE /api/qr/{token}
GET /api/qr/{token}/image
GET /api/qr/{token}/analytics
這組 API 把「使用者掃描」和「管理 QR Code」分開:
/r/{token}給掃描者使用,只負責 redirect。/api/qr/*給管理端使用,負責建立、查詢、更新、刪除和分析。
這樣路由語意很清楚,後續要加權限也比較容易。
建立 QR Code
建立時會先驗證 URL,再產生 token:
@router.post("/api/qr/create", response_model=CreateResponse)
def create_qr(req: CreateRequest, db: Session = Depends(get_db)):
normalized_url = validate_url(req.url)
token = generate_token(normalized_url, db)
mapping = UrlMapping(
token=token,
original_url=normalized_url,
expires_at=req.expires_at,
)
db.add(mapping)
db.commit()
short_url = f"{BASE_URL}/r/{token}"
redirect_cache[token] = normalized_url
return CreateResponse(
token=token,
short_url=short_url,
qr_code_url=f"{BASE_URL}/api/qr/{token}/image",
original_url=normalized_url,
)
這裡回傳三個很實用的欄位:
token:資料庫和管理 API 使用。short_url:實際被 QR Code 編碼的網址。qr_code_url:取得 PNG 圖片的 endpoint。
QR 圖片只放短網址
產生圖片的 endpoint 很單純:
@router.get("/api/qr/{token}/image")
def get_qr_image(token: str, db: Session = Depends(get_db)):
_get_mapping_or_404(token, db)
short_url = f"{BASE_URL}/r/{token}"
img = qrcode.make(short_url)
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/png")
重點是圖片裡永遠只放 short URL。
當管理者之後更新 original_url 時,QR 圖片不用變,只要 redirect target 變就好。
Database Model
URL mapping 保存 token 和目標網址:
class UrlMapping(Base):
__tablename__ = "url_mappings"
id = mapped_column(Integer, primary_key=True, autoincrement=True)
token = mapped_column(String(8), unique=True, nullable=False, index=True)
original_url = mapped_column(Text, nullable=False)
expires_at = mapped_column(DateTime, nullable=True)
is_deleted = mapped_column(Boolean, default=False)
這裡的 expires_at 和 is_deleted 讓 QR Code 有生命週期管理,而不只是永久 redirect。
使用情境
Dynamic QR Code 很適合這些場景:
- 活動海報:活動頁網址可能調整。
- 商品包裝:產品頁或促銷頁可能換檔期。
- 餐廳菜單:菜單內容常更新。
- 內部文件:掃描後連到最新版本文件。
- 行銷追蹤:需要知道掃描量和日期分布。
真正的價值不是 QR Code 本身,而是 QR Code 背後多了一個可管理的 redirect layer。
總結
Dynamic QR Code 的架構其實很直覺:
QR Code -> short URL -> server -> original URL
但多了這一層,就得到三個重要能力:
- 目標網址可更新。
- 掃描行為可追蹤。
- QR Code 可過期或停用。
下一篇會繼續看 token 怎麼產生,以及為什麼 prototype 使用 SHA-256、nonce 和 Base62。