這道題是 Stripe 訂閱系統(Billing)團隊的經典面試題。它考察的是事件調度(Scheduling)、時間軸排序以及狀態變更的傳播。
題目描述
Stripe 需要給訂閱用戶發送通知郵件。每個用戶都有一個訂閱計劃(Plan),且不同的時間節點需要發送不同的郵件。
Part 1: 靜態調度 (Static Schedule)
給定一個發送計劃模板(Schedule Template)和一組用戶訂閱資訊,請輸出所有需要發送的郵件日誌,按時間排序。
發送計劃模板:
Start: "Welcome email"T-15(到期前15天): "Upcoming expiry"End: "Subscription expired"
用戶數據:
- Alice: Start=0, Duration=30 days
- Bob: Start=10, Duration=30 days
預期輸出: 你需要計算出每個用戶的具體發送日期(Day 0, Day 15, Day 30),將所有用戶的郵件事件合併,並按時間戳排序輸出。
Part 2: 動態變更 (Dynamic Changes)
用戶可能會在訂閱中途更改 Plan(例如從 Silver 升級到 Gold)。 新規劃:
- 如果用戶更改了 Plan,系統需要立即發送一封 "Plan Changed" 郵件。
- 後續的所有定時郵件(如 "Expired")都需要在主題中反映最新的 Plan 名稱。
輸入增加:
PlanChanges = [{user: "Alice", time: 5, new_plan: "Gold"}]
難點: 你需要動態更新 Alice 在 Day 15 和 Day 30 的待發送郵件內容。
oavoservice 解題思路分析
這道題本質上是一個優先隊列(Priority Queue)或事件合併排序問題。
1. 資料結構設計
我們需要一個物件來表示「待發送郵件」:
class EmailEvent:
time: int
user_id: str
template_type: str # e.g., WELCOME, EXPIRY
original_plan: str
# 用於排序
def __lt__(self, other):
return self.time < other.time
2. 處理流程
- 初始化:遍歷所有用戶,根據模板生成初始的 EmailEvents 列表。
- 合併變更:遍歷 PlanChanges,生成 "Plan Changed" 事件加入列表。
- 狀態回溯(關鍵):
- 最簡單的做法是:在生成輸出時,即時查詢該用戶在
event.time時刻的有效 Plan。 - 或者:維護一個
User -> CurrentPlan的映射。按時間順序處理所有事件(包括郵件發送事件和 Plan 變更事件)。
- 最簡單的做法是:在生成輸出時,即時查詢該用戶在
3. 演算法選擇
- 方法 A (排序):將所有 PlanChange 事件和 Email 事件放入同一個列表,按
time排序。遍歷一遍,遇到 Change 事件就更新current_plan狀態,遇到 Email 事件就用當前狀態列印日誌。- 時間複雜度:
O(N log N),其中 N 是總事件數。 - 這也是最推薦的面試解法。
- 時間複雜度:
程式碼片段 (Python)
def generate_notifications(users, schedule, changes):
events = []
# 1. 生成基礎郵件事件
for u in users:
start = u['start']
end = start + u['duration']
# Add Welcome
events.append({"time": start, "type": "EMAIL", "msg": "Welcome", "user": u})
# Add Expiry
events.append({"time": end, "type": "EMAIL", "msg": "Expired", "user": u})
# 2. 生成變更事件
for c in changes:
events.append({"time": c['time'], "type": "CHANGE", "new_plan": c['new_plan'], "user_name": c['name']})
# 3. 排序
events.sort(key=lambda x: x['time'])
# 4. 模擬時間軸
user_plans = {u['name']: u['plan'] for u in users}
for e in events:
if e['type'] == "CHANGE":
user_plans[e['user_name']] = e['new_plan']
print(f"{e['time']}: Plan Changed for {e['user_name']} to {e['new_plan']}")
else:
current_plan = user_plans[e['user']['name']]
print(f"{e['time']}: {e['msg']} for {e['user']['name']} ({current_plan})")
想知道如何處理更複雜的調度?
如果發送時間是「每月的第某個週一」怎麼辦?如果時區不同怎麼辦? oavoservice 的面試輔助服務會帶你深入探討這些 System Design 級別的 Follow-up,確保你在面試中不僅能寫出程式碼,還能展現架構能力。