iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Claude AI

Claude Code陪跑:一人開發者的訂閱制SaaS架構與十大地雷實戰記系列 第 16

# Day 16|點數機制:先寫紀錄再發點,順序不能反的理由

  • 分享至 

  • xImage
  •  

從一個看似簡單的需求開始

服務裡有一套點數機制:完成某些里程碑(首次設定、連續使用等)發放點數,點數可以兌換進階內容。需求聽起來是 CRUD 入門題:「達成條件 → 發點數」。

但「發放型」邏輯藏著一個所有支付、獎勵、優惠券系統共通的深坑:重複發放

天真版的事故劇本

def grant_milestone_reward(user, milestone):
    if has_granted(user, milestone):   # 檢查發過沒
        return
    add_points(user, amount)           # 發點
    record_grant(user, milestone)      # 記錄

看起來滴水不漏?劇本來了:使用者的網路卡頓、連點兩下按鈕、兩個請求幾乎同時到達。兩個請求都跑 has_granted——都回傳 False(因為誰都還沒寫紀錄)——於是兩個請求都發了點

這不是理論風險。行動網路環境下的重複請求是日常,而點數直接對應真金白銀的成本(兌換的進階內容有 AI 呼叫費用)。重複發放不會報錯、不會當機,只會在帳務上安靜地失血。

正確的順序:讓資料庫當守門員

修法的核心是調換順序,並且讓「防重」由資料庫的唯一性約束來保證

CREATE TABLE reward_grants (
    username   TEXT,
    milestone  TEXT,
    granted_at TIMESTAMP,
    UNIQUE (username, milestone)   -- 同一人同一里程碑,物理上只能有一筆
);
def grant_milestone_reward(user, milestone):
    try:
        insert_grant_record(user, milestone)   # 第一步:先搶佔紀錄
    except UniqueViolation:
        return                                  # 搶輸了 = 已經發過,安靜退出
    add_points(user, amount)                    # 第二步:搶贏的才發點

兩個併發請求同時 INSERT,資料庫的唯一性約束保證只有一個會成功——這是資料庫給你的原子性承諾,不需要應用層加鎖。搶贏的發點,搶輸的收到違反約束的錯誤,安靜退出。

順序為什麼不能反?如果先發點再寫紀錄,「發了點但還沒寫紀錄」的空窗期裡,任何重複請求都能鑽進去。而反過來的失敗模式是「寫了紀錄但發點失敗」——這是少發,使用者會回報,補發就好;多發則無聲無息,你只會在成本報表上看到結果。兩種失敗模式的可觀測性天差地遠,永遠選擇會被使用者回報的那種失敗

通則

所有「發放型」動作(點數、額度、優惠券、通知)的安全結構是同一個:

  1. 防重紀錄先行,用資料庫唯一性約束當唯一裁判
  2. 應用層的 has_granted 檢查可以留著當快速路徑,但不能是唯一防線
  3. 設計失敗模式時,讓錯誤偏向「少給」而不是「多給」

這條也進了地雷清單,一句話版本:「發放類:先 INSERT 防重紀錄、再發放,順序不能反。」


上一篇
# Day 15|金流地基先蓋好,等條件成熟再開燈
系列文
Claude Code陪跑:一人開發者的訂閱制SaaS架構與十大地雷實戰記16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言