这道题是 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,确保你在面试中不仅能写出代码,还能展现架构能力。