前几篇写了功能原型、技术方案、用户调研。现在到了最硬核的部分——组局的业务流程状态机。
一个产品好不好,70% 看设计,30% 看代码。但状态机写错了,产品从第一天起就是错的。
这篇文章我打算讲透:一个组局从”我想打场球”到”打完球各回各家”,中间经历了哪些状态,状态之间怎么跳转,跳转的触发条件是什么,以及 30 多个边角case怎么处理。
一、为什么状态机是这个产品最难的部分
社交产品的状态机通常很简单——发帖、删帖、评论、点赞。
组局产品的状态机复杂得多,因为:
涉及多个角色
组织者、参与者、旁观者,每个人对同一场活动看到的界面可能不同。
涉及时间维度
活动有明确的开始时间和结束时间。很多状态变化是时间触发的,不是用户触发的。
涉及金钱(哪怕只是场地费)
报名者付了钱,如果活动取消,钱怎么办?如果参与者缺席,组织者亏的怎么办?
涉及线下行为
线上报名了不等于线下到场了。怎么确认”这个人真的来了”?怎么确认”这个人真的没来”?
涉及信任机制
参与者敢不敢去一个只有陌生人的活动?组织者敢不敢取消一个已经报了 5 个人的活动?
状态机设计错了,这些风险都会以”用户投诉”的形式爆出来。
二、核心状态机:Activity(组局)
组局是整个业务的核心实体。一张 Activity 表,一条记录代表一个组局。
状态列表
| 状态 | 说明 | 谁可见 | 可否报名 |
|---|---|---|---|
draft |
草稿,只有组织者能看到 | 仅组织者 | 否 |
open |
开放报名中 | 所有人 | 是 |
ongoing |
活动中 | 所有人 | 否 |
completed |
活动已完成 | 所有人 | 否 |
canceled |
已取消 | 所有人 | 否 |
no_show |
无人报名自动取消 | 所有人 | 否 |
状态转移图
1 | ┌──────────┐ |
详细状态转移表
| 从 | 到 | 触发条件 | 动作 |
|---|---|---|---|
| draft | open | 组织者点击”发布” | 组局对所有人可见,通知系统记录开放时间 |
| open | ongoing | NOW() >= start_time(系统定时任务) |
活动开始,通知所有已报名者”活动开始”,停止接受报名 |
| open | canceled | 组织者点击”取消” | 所有报名者收到通知,Apply 状态全部变为 canceled |
| open | no_show | 开始时间前 30 分钟,报名人数 = 0 | 自动取消,组织者收到”活动取消”通知 |
| open | draft | 组织者点击”修改并发布”(重发) | 修改后重新发布为新的 open 状态(版本号 +1) |
| ongoing | completed | 组织者点击”签到完成” | 记录完成时间,触发评价请求,通知所有参与者”活动已完成” |
| ongoing | completed | NOW() >= end_time + 24h 且组织者未签到 |
系统自动标记完成(防止组织者忘记操作) |
| ongoing | canceled | 组织者点击”取消”(活动中) | 特殊情况:活动中取消。所有 Apply 状态变为 canceled,组织者信用分 -15 |
三个时间触发的边界条件
状态机里有些转移是时间触发的,不是用户触发的。这涉及到三个时间窗口:
窗口 1:报名截止
open 状态持续到 start_time - 30 分钟。
在这 30 分钟内:
- 如果报名人数 ≥ 2(最低要求),继续等待开始
- 如果报名人数 = 0,自动触发 open → no_show
- 如果报名人数 = 1,允许开始(组织者自己一个人也能打)
窗口 2:活动中签到
ongoing 状态持续到 end_time。
在 start_time 到 end_time 之间:
- 参与者可以签到(Apply 状态从 applied → checked_in)
- 组织者可以点击”签到完成”触发 ongoing → completed
- 参与者不可以取消报名
窗口 3:活动结束后自动完成
end_time + 24 小时:
- 如果组织者还未点击签到完成,系统自动将 ongoing → completed
- 组织者收到通知:”你的活动已被系统自动标记为完成,请尽快补充评价”
为什么要 24 小时自动完成
一个组织者打完球,可能手机没电了,或者忘了点”完成”。如果不做自动完成,活动会永远卡在 ongoing 状态。
24 小时是合理的:
- 组织者有足够时间回来操作
- 参与者不会等太久才收到”活动已完成”的通知
- 给组织者足够时间回想活动细节再评价
三、核心状态机:Apply(报名)
一个 Activity 对应多个 Apply。Apply 状态机比 Activity 复杂得多,因为涉及”组织者确认”和”缺席判定”。
状态列表
| 状态 | 说明 |
|---|---|
applied |
已报名,组织者尚未确认 |
confirmed |
组织者已确认(可选步骤) |
checked_in |
已到场签到 |
absent |
缺席(活动完成时仍未签到) |
canceled |
已取消报名 |
no_show |
报名但组织者取消活动,自动取消 |
报名确认机制:要不要组织者确认?
这是一个关键的产品设计决策。
方案 A:报名即加入,无需确认
用户点击报名,立即进入 Apply = confirmed 状态。
优点:简单、快速、转化率高
缺点:组织者无法筛选参与者,可能被不想跟的人加入
方案 B:报名需组织者确认
用户点击报名,进入 Apply = applied 状态。组织者审核后,确认或拒绝。
优点:组织者有控制权,可以拒绝素质差的人
缺点:多了确认步骤,用户可能觉得麻烦
建议:混合模式
- 默认:报名即加入(不确认)
- 特殊情况:组织者可以手动拒绝某个报名者(比如报名者信用分太低)
- 人数已满后:后来报名者进入”等待名单”
具体规则:
| 场景 | 处理方式 |
|---|---|
| 报名者 < 人数上限 | 立即 confirmed |
| 报名者 = 人数上限 | 后来者进入 waitlist |
| waitlist 有人取消 | 按时间顺序从 waitlist 补位 |
| 组织者手动拒绝 | applied → rejected(不计入人数) |
Apply 状态转移图
1 | ┌──────────┐ |
Apply 状态转移表
| 从 | 到 | 触发条件 | 动作 |
|---|---|---|---|
| applied | confirmed | 报名人数 < 上限,自动确认 | Apply 加入计数,通知组织者”新增报名者” |
| applied | waitlist | 报名人数 = 上限 | Apply 加入等待名单,不计入人数 |
| applied | rejected | 组织者手动拒绝 | 不计入人数,报名者收到通知 |
| applied | canceled | 用户主动取消(开始时间前 2h) | 不计入人数,释放名额 |
| confirmed | canceled | 用户主动取消(开始时间前 2h) | 不计入人数,释放名额,如有 waitlist 则补位 |
| confirmed | checked_in | 活动进行中,用户点击签到 | 参与计数 +1 |
| confirmed | absent | 活动完成时未签到 | 组织者可以标记为 absent,或系统自动标记 |
| confirmed | canceled | 组织者取消活动 | 自动取消,不计入人数 |
| waitlist | confirmed | 有人退出,按顺序补位 | 从 waitlist 转为 confirmed,通知补位者 |
四、组织者权限:不同状态下的可用操作
组织者在不同状态下能做什么操作,这是最容易出 bug 的地方。
| Activity 状态 | 组织者能做的操作 |
|---|---|
| draft | 修改标题、时间、地点、人数、费用、备注;删除 |
| open | 查看报名者列表;手动拒绝某个报名者;取消活动 |
| open | 修改时间/地点(修改后所有报名者重新确认) |
| ongoing | 点击”签到完成”(结束活动);取消活动(活动中) |
| ongoing | 给缺席者标记 absent(手动) |
| completed | 查看签到情况;编辑活动备注 |
| canceled | 无操作 |
| no_show | 无操作 |
时间修改的特殊处理
组织者在 open 状态下修改活动时间,需要分两种情况:
情况一:延后
- 修改后的开始时间在当前时间之后
- 所有已报名者的报名继续有效
- 发送”活动已延期”通知给所有报名者
情况二:提前
- 修改后的开始时间早于当前时间
- 不允许修改(时间不能倒流)
- 提示”请选择开始时间在当前时间之后的时间段”
情况三:大幅修改(比如改天、改场地)
- 视为”新活动”,原活动自动 canceled
- 组织者需要重新创建
- 原报名者收到通知并可选择报名新活动
规则:同一活动最多延期一次。避免组织者反复修改时间导致参与者混乱。
五、缺席判定机制
“这个人报了名但没来”——缺席怎么判定?
这是产品设计中争议最大的问题。
缺席的两种判定方式
方式一:组织者手动标记
活动完成后,组织者点击”签到完成”时,系统弹出报名者列表,组织者逐个标记”到场”或”缺席”。
优点:准确,组织者最了解现场情况
缺点:增加组织者操作负担,且组织者可能忘记标记
方式二:参与者主动签到
活动进行中,参与者点击”我已到场”完成签到。活动结束时,未签到的人自动判定为缺席。
优点:减轻组织者负担,数据自动采集
缺点:参与者可能忘记签到,导致”误判缺席”
建议:两者结合
- 活动开始前 30 分钟,参与者可以通过 App 签到(Apply:applied → checked_in)
- 活动进行中,组织者可以点击”签到完成”时确认实际到场名单
- 组织者标记的名单优先级高于用户签到
- 如果组织者未标记缺席,系统按签到状态判定
缺席的信用影响
| 行为 | 信用分变化 |
|---|---|
| 准时签到 | +5 |
| 缺席(活动完成时未签到) | -10 |
| 报名后 24 小时内取消 | 不影响信用分 |
| 报名后 2 小时内取消 | -3 |
| 活动开始后取消 | -15 |
| 组织者取消活动(非不可抗力) | -15 |
| 收到参与者好评 | +5 |
| 收到参与者差评 | -5 |
信用分范围:50-150。低于 80 分,组织者无法发起组局,参与者无法报名他人组局。
缺席的上限保护
避免恶意标记:
- 组织者最多在一个活动中标记 3 个人为”缺席”(防止误操作或报复)
- 被标记缺席的用户可以在 24 小时内申诉(说明理由,由平台客服审核)
六、报名取消规则
取消规则是信任机制的核心。
| 距开始时间 | 用户可否取消 | 信用影响 | 退款/费用处理 |
|---|---|---|---|
| > 2 小时 | 可自由取消 | 无影响 | 如有费用,全额退款 |
| 1-2 小时 | 可取消 | -3 分 | 如有费用,退 50% |
| 30 分钟-1 小时 | 可取消 | -10 分 | 如有费用,不退 |
| < 30 分钟 | 不可取消 | N/A | N/A |
| 活动开始 | 不可取消 | N/A | N/A |
规则设计逻辑:越接近开始时间,取消成本越高。这样阻止”占着茅坑不拉屎”的行为。
组织者也需要遵守类似规则:
| 组织者行为 | 信用影响 |
|---|---|
| 开放报名 24 小时后取消活动 | -10 分 |
| 活动中取消 | -15 分 |
| 连续 3 次取消活动 | 7 天禁止发起组局 |
| 不可抗力取消(提供证据) | 不影响信用分 |
七、信用分计算细节
信用分是陌生人社交的”信任货币”。
基础规则
初始分数:100 分。
加分:
- 准时签到:+5
- 收到好评:+5
- 连续 3 次准时参加:+10
减分:
- 缺席:-10
- 迟到签到(超过开始时间 30 分钟):-5
- 取消报名(2 小时内):-3
- 收到差评:-5
- 组织者取消活动:-15
加分和减分有上限:
- 单次活动最高 +10 分
- 单次活动最高 -20 分
信用分阈值与功能限制
| 信用分 | 可发起组局 | 可报名 | 可查看个人主页 |
|---|---|---|---|
| ≥ 100 | 可 | 可 | 可 |
| 80-99 | 可 | 可 | 可(但显示”信用良好”) |
| 60-79 | 不可(需审核) | 可 | 可(显示”信用一般”) |
| < 60 | 不可 | 不可 | 可(显示”信用警告”) |
信用分的展示策略
不要在个人主页直接展示”信用分:87”——这个数字对用户没有意义。
改为展示标签:
| 信用分 | 标签 |
|---|---|
| ≥ 120 | “金牌组局达人” |
| 100-119 | “靠谱” |
| 80-99 | “信用良好” |
| 60-79 | “信用一般” |
| < 60 | “信用警告” |
标签比数字更友好。
八、评价系统的状态机
评价也是独立的状态机。
评价状态
| 状态 | 说明 |
|---|---|
pending |
活动完成,尚未评价 |
reviewed |
已评价 |
disputed |
申诉中 |
resolved |
申诉已处理 |
评价的触发时机
- 组织者点击”签到完成”后,所有参与者的评价进入
pending状态 - 组织者未签到,24 小时后系统自动标记 completed,评价进入
pending - 评价请求发出后 48 小时,
pending状态自动关闭(不再要求评价)
评价的内容限制
| 规则 | 说明 |
|---|---|
| 最低评价字数 | 5 字(避免”好”这样无意义评价) |
| 最高评价字数 | 200 字 |
| 关键词过滤 | 黑名单词自动过滤(脏话、人身攻击) |
| 举报机制 | 任何评价可被举报,管理员审核 |
双向评价
活动完成后,参与者评价组织者,组织者也可以评价参与者。
但组织者只能评价签到过的参与者——缺席者没有评价资格(防止报复性差评)。
九、30 个边角 case 清单
以下这些是状态机设计时必须提前想到的边角情况:
关于报名
- 报名者在同一活动中重复报名(同一个人)→ 前端防抖,后端唯一约束
- 组织者给自己报名的活动报名 → 不允许
- 报名者被组织者拒绝后,是否还能再次报名 → 允许,视为新的报名
- waitlist 补位时,原 canceled 的人再次报名 → 不允许进入同一活动
- 活动人数上限为 0 时 → 不发布(前端校验)
- 组织者报名自己的活动后,是否占用名额 → 不占用
关于取消
- 活动开始前 1 分钟组织者取消 → 全部报名者通知,信用分 -10
- 活动已开始,组织者点击取消 → 视为”活动中取消”,信用分 -15
- 活动已完成,组织者再次点击取消 → 不允许
- 报名者在活动进行中点击取消 → 不允许
关于时间
- 组织者修改时间为过去的时间 → 不允许,提示
- 组织者修改时间为同一个时间 → 无变化
- 组织者修改时间后,原报名者的确认状态 → 所有报名者重新确认(已过期)
- 活动结束时间早于开始时间 → 不允许发布
- 活动时长不足 1 小时 → 不允许发布(太短无法组局)
- 活动时长超过 8 小时 → 不允许发布(过长)
- 活动开始时间距当前时间不足 1 小时 → 允许但提示”时间较近,请确认”
关于信用
- 信用分为 0 时,用户还能干什么 → 只能浏览,不能报名、不能发起
- 用户信用分从 100 分跌至 59 分(-21 分),是否立即限制 → 立即限制
- 用户申诉成功后,信用分是否恢复 → 恢复
关于数据一致性
- 组织者点击签到完成,但参与者未签到 → 组织者可以手动标记缺席
- 组织者未签到,24 小时后系统自动完成,参与者能否补签到 → 不允许
- 同一用户同时参与两个时间重叠的活动 → 允许(系统提示冲突,不阻止)
- 用户删除账号后,已报名的活动如何处理 → 报名自动取消
关于系统级
- 服务器宕机,活动正在进行中 → 服务恢复后,状态不变
- 数据库写入失败(网络问题)→ 前端提示”操作失败,请重试”
- 用户离线状态下修改报名状态 → 下次上线时同步
- 多个用户同时点击同一个活动的报名按钮 → 用数据库唯一约束防止超报
- 活动结束后,组织者重新打开评价页面 → 只读,不可再编辑
- 组织者修改活动信息后,已报名者是否需要重新确认 → 如果修改了时间/地点,需要;如果只改了备注,不需要
十、实现建议
后端:用状态机引擎,不要用 if-else 堆
状态转移用代码实现时,不要用 20 个 if-else。
推荐用有限状态机库(比如 xstate 或自写一个简单的状态机引擎):
1 | interface Transition { |
这样状态转移逻辑是可读、可测试、可扩展的。
定时任务
需要两个定时任务:
定时任务 1:活动自动开始
每 5 分钟检查一次,把 start_time <= NOW() 且状态为 open 的活动标记为 ongoing。
1 | UPDATE activities |
注意:如果活动人数为 0,应标记为 no_show 而非 ongoing。
定时任务 2:活动自动完成
每 30 分钟检查一次,把 end_time + 24h <= NOW() 且状态为 ongoing 的活动标记为 completed。
1 | UPDATE activities |
关键数据一致性
报名状态的一致性最重要。用数据库事务保证:
1 | async function applyActivity(userId: string, activityId: string) { |
十一、最后说
状态机是这个产品最”枯燥”的部分,但也是最决定性的部分。
功能设计错了,可以改。技术选型错了,可以重构。但状态机错了,用户数据就会错——报名了显示没报名,活动结束了还在进行,缺席的人显示已签到。
这种错误一旦发生,信任就没了。
写状态机的时候,多花一点时间把边角 case 想到。上面列的 30 个 case,上线前至少验证过 20 个,你才不会在上架后因为一个边界 bug 丢一批用户。
状态机写好了,这个产品就有了骨架。后面加什么功能,都可以在这套骨架上长。