当前位置: 首页 > news >正文

后端复盘(4):阶段结束不等于流程结束,一个 finished 字段为什么不够用

  • 本文来自《后端系统设计复盘:从游戏项目到通用后端》专栏。
  • 文章内容源于我正在实现的一个 Balatro 风格实时游戏后端项目,但本文不会重点讲某个具体游戏功能怎么写,而是抽出一个更通用的后端设计问题:当一个系统里同时存在“小阶段结束”和“大流程结束”时,为什么不能只用一个状态字段硬扛。
  • 这个问题不只存在于游戏后端,在订单系统、审批流程、活动任务、工作流系统里也很常见。很多状态混乱,不是因为代码写错了,而是因为一个字段被迫表达了太多层含义。

⚠️ 比如finished = true,到底表示什么?

  • 当前操作完成了?
  • 当前阶段结束了?
  • 整个流程结束了?
  • 只是进入了结算页?

如果这些问题需要靠上下文猜,那这个状态字段就已经开始失控了。


这篇文章想表达的核心观点是:

  • 当系统里同时存在“当前阶段结束”和“整体流程结束”时,不能只用一个gameOverfinisheddone来硬扛。
  • 不同层级的结束,往往代表不同的后续动作:有的要结算,有的要推进,有的要保留状态,有的要清理状态。
  • 生命周期拆分的重点,不是多加几个字段,而是让系统知道:当前结束的是哪一层流程,以及接下来应该做什么。

文章目录

  • 一、问题是怎么出现的
    • 1. 早期只有一层结束状态
    • 2. 多阶段流程出现后,问题开始暴露
    • 3. 为什么这不是游戏特有问题
  • 二、为什么一个 finished 字段不够用
    • 1. 它表达不了“阶段结束但流程未结束”
    • 2. 代码会开始依赖组合判断
    • 3. 前端和服务端都会开始猜语义
  • 三、阶段结束和流程结束到底差在哪
    • 1. 三种结束不是同一层概念
    • 2. 阶段结束只是节点完成,流程结束才是链路完成
  • 四、拆分后,状态应该怎么表达
    • 1. 用 stageOver 表达当前阶段是否结束
    • 2. 用 flowOver 表达整体流程是否结束
    • 3. 不要用 finished + result 反推状态层级
  • 五、拆分前后,流程判断有什么变化
    • 1. 拆分前:一个 gameOver 承载所有结束含义
    • 2. 拆分后:先判断阶段,再判断整体流程
  • 六、结束之后,哪些状态保留,哪些状态清理
    • 1. 阶段胜利:保留跨阶段状态,清理阶段内状态
    • 2. 阶段失败:进入整体结束,清理运行时状态
    • 3. 每次结束后都要问的几个问题
  • 七、通用后端里也很常见
    • 1. 订单系统:支付完成不等于订单完成
    • 2. 审批系统:一审通过不等于审批完成
    • 3. 活动任务:单个任务完成不等于整个活动结束
  • 八、状态命名要表达层级
    • 1. 太泛的命名会被复用成多种含义
    • 2. 字段名要带上生命周期上下文
  • 九、不要急着上复杂状态机,但要先分清层级
    • 1. 简单流程,一个 status 就够用
    • 2. 多层流程出现后,就不能再硬扛
    • 3. 要避免两个极端
  • 十、本篇小结

一、问题是怎么出现的

  • 下面我会用一个游戏里的多阶段流程举例,但这里讨论的不是游戏特有问题,而是很多后端系统都会遇到的“多层生命周期”问题
  • 在订单系统里,它可能是“支付完成不等于订单完成”;在审批系统里,它可能是“一审通过不等于整个审批通过”;在活动系统里,它可能是“单个任务完成不等于整个活动结束”。

为了让这篇文章可以独立阅读,先解释几个后面会出现的词。

名词在本文里的含义普通业务里的类比
Blind游戏里的一个阶段关卡一个任务阶段、一个订单节点、一个审批节点
small blind/big blind/boss blind当前游戏流程中的不同阶段普通任务、关键任务、最终节点
ante更大的进度层级,可以理解成一组阶段第几轮、第几期、第几个大流程
gameOver当前游戏整体是否结束订单是否结束、审批是否结束、活动是否结束
生命周期一个状态从开始、运行、结束到清理的过程订单创建、支付、发货、完成、取消

1. 早期只有一层结束状态

在项目早期,流程还比较简单,所以我当时并没有把“结束状态”拆得很细。

那时候系统更像是一个单局流程:用户开始一局游戏,然后出牌、弃牌、补牌、算分,最后判断这一局是赢了还是输了。

在这种复杂度下,一个gameOvergameStatus确实够用。

比如:gameOver: boolean;

或者:gameStatus: "playing" | "finished";

因为那个时候,流程大概是这样的:开始游戏 -> 用户操作 -> 判断是否结束 -> 结束后返回结算

整个系统只有一层生命周期:

当前这一局是否结束。

2. 多阶段流程出现后,问题开始暴露

但后面继续往下做的时候,流程不再只有一层,“结束”这个词开始出现不同含义。因为系统开始不只是“一局打完就结束”,而是出现了多个阶段。

比如:small blind -> big blind -> boss blind -> next ante

这时候,某一个阶段结束,并不代表整个流程结束。

用户通过当前阶段后,应该进入下一个阶段。只有用户失败,或者整个大流程达到最终条件时,整体流程才真正结束。

也就是说,系统里开始出现了两种“结束”:

  • 当前阶段结束
  • 整体流程结束

这个时候,如果还只用一个gameOver,就会开始变得很别扭。

因为它已经表达不了真实业务含义了。

3. 为什么这不是游戏特有问题

虽然这个问题是在游戏流程里暴露出来的,但它并不是游戏后端特有的问题。

只要一个系统里存在“节点推进”或“阶段流转”,就很容易出现类似情况:某个小阶段已经完成,但整个业务流程还没有结束。

比如订单系统里,支付完成不等于订单完成;审批系统里,一审通过不等于整个审批通过;活动任务里,单个任务完成也不等于整个活动结束。


二、为什么一个 finished 字段不够用

1. 它表达不了“阶段结束但流程未结束”

这里的gameOver只是当前项目里的字段名。放到更通用的系统里,它可能叫finisheddonecompletedprocessOver

一开始只有gameOver时,它的含义很清楚:游戏是否结束。

但当系统开始出现阶段推进后,gameOver就开始有点尴尬了。

比如当前阶段通过了,这个时候当前阶段确实结束了,但整场流程并没有结束。

如果把gameOver设成true,前端可能会以为整场游戏结束了;如果把gameOver设成false,又表达不出当前阶段已经结算完成。

这就说明一个问题:

一个状态字段正在被迫表达两层含义。

2. 代码会开始依赖组合判断

这种状态很危险,因为它会让代码开始出现很多奇怪判断。

比如:

if(gameOver&&result==="WIN"){// 其实不是整场结束,只是当前阶段结束}if(gameOver&&result==="LOSE"){// 这次才是真的整场结束}

看起来能写,但语义已经开始混了。

真正的问题不是代码能不能跑,而是:

字段名字和业务含义已经对不上了。

3. 前端和服务端都会开始猜语义

如果继续用一个gameOver硬扛,至少会带来三个问题:

问题具体表现
前端不知道展示什么gameOver = true到底是阶段结算页、失败页,还是最终结束页
服务端推进逻辑混乱阶段胜利要推进,阶段失败要清理,但字段表达不出来
后续扩展越来越痛加奖励、商店、临时效果时,只能继续补判断

比如gameOver = true时,前端到底应该展示当前阶段结算页、整场失败页、下一阶段入口,还是最终结束页?

如果还要再看resultrewardnextStage等字段才能猜出来,那前端逻辑就会越来越依赖隐含规则。

服务端自己也会遇到类似问题。当前阶段结束后,有些情况要保留状态,有些情况要清理状态。如果只用一个gameOver,代码里就很容易把“阶段结算”和“整体结束”混在一起。

所以这类字段不是不能继续加判断,而是每次改都很怕影响别的流程。


三、阶段结束和流程结束到底差在哪

1. 三种结束不是同一层概念

我后来才更明确地意识到:

阶段结束和流程结束,是两个不同层级的生命周期。

它们不是同一个概念。

可以先做一个简单对比:

生命周期层级表示什么结束后通常做什么典型例子
操作结束一次请求或动作处理完成返回本次操作结果,更新局部状态一次出牌、一次提交、一次支付请求
阶段结束当前小阶段完成或失败阶段结算、准备下一阶段、重置阶段状态当前 Blind 结束、审批节点完成、任务完成
流程结束整个业务链路完成或失败返回最终结果、清理运行时状态、持久化结果游戏失败、订单完成、审批结束、活动结束

2. 阶段结束只是节点完成,流程结束才是链路完成

放回当前项目里,就会更明显:

场景当前阶段是否结束整体流程是否结束应该怎么处理
当前阶段分数达标结算当前阶段,进入下一阶段
当前阶段失败返回失败结果,清理运行时状态
当前阶段还在进行中返回当前状态,继续操作
最终一个阶段通过可能是根据规则进入下一大阶段或最终结算

也就是说:

阶段结束 = 当前节点完成 流程结束 = 整条链路完成

这两个状态有关系,但不能混成一个字段。

如果换到普通业务系统里,也很好理解。

订单里“支付成功”只是支付阶段结束,不代表订单完成;审批里“一审通过”只是当前节点完成,不代表整个审批通过;活动里“任务 A 完成”只是一个任务完成,不代表整个活动结束。

所以生命周期拆分的关键不是字段数量,而是语义层级。


四、拆分后,状态应该怎么表达

1. 用 stageOver 表达当前阶段是否结束

更合理的做法,是把生命周期拆开。

比如:

typeProgress={stageOver:boolean;flowOver:boolean;};

放回当前项目里,可以是:

typeProgress={blindOver:boolean;gameOver:boolean;};

这里的重点不是字段名,而是语义:stageOver/blindOver:当前阶段是否结束

2. 用 flowOver 表达整体流程是否结束

另一个字段则专门表达整体流程是否已经结束,也就是:flowOver/gameOver表示整体流程是否结束

这样状态表达会清楚很多。

stageOverflowOver含义后续动作
falsefalse当前阶段继续返回当前状态
truefalse当前阶段完成,但整体流程继续结算阶段,推进下一阶段
truetrue当前阶段完成,整体流程结束返回最终结果,清理状态
falsetrue通常不合理需要检查状态设计

这里最关键的不是字段名,而是状态层级:

stageOver表达当前阶段是否结束,flowOver表达整体流程是否结束。它们可以有关联,但不应该被混成一个字段。

如果出现:

stageOver = false flowOver = true

大多数时候都说明状态设计有问题。因为整体流程都结束了,当前阶段一般也应该已经结束。

3. 不要用 finished + result 反推状态层级

如果只用一个finished,后面很容易写成这样:

typeProgress={finished:boolean;result?:"WIN"|"LOSE";};

这时finished = true还不够,还要继续看result才能判断下一步。

字段组合可能含义
finished = true+WIN可能只是当前阶段通过
finished = true+LOSE可能才是整体流程结束

字段本身没有表达清楚状态层级,只能靠组合判断补语义。

更清楚的写法是:

typeProgress={stageOver:boolean;flowOver:boolean;result?:"WIN"|"LOSE";};

这样前端和服务端都能直接知道:当前结束的是阶段,还是整个流程。


五、拆分前后,流程判断有什么变化

1. 拆分前:一个 gameOver 承载所有结束含义

拆分之前,流程可能是这样的:

false

true

用户操作

计算结果

gameOver?

继续当前流程

结束并返回结算

这个结构在单局阶段没问题。

但一旦进入多阶段,它就开始不够用了。

因为gameOver = true可能表示:

  • 当前阶段结束
  • 整体流程结束
  • 当前阶段胜利
  • 当前阶段失败

这些含义都挤在一起了。

2. 拆分后:先判断阶段,再判断整体流程

拆分后,流程会变成两层判断。

第一层判断:

当前阶段有没有结束?

第二层判断:

整体流程有没有结束?

可以用一张图表示:

一次操作完成

当前阶段是否结束

返回最新状态
继续当前阶段

生成阶段结算

整体流程是否结束

保留跨阶段状态
推进下一阶段

生成最终结果
清理运行时状态

这张图其实就是多层生命周期最核心的思路:

  • 操作完成,不代表阶段结束
  • 阶段结束,不代表流程结束
  • 流程结束,才意味着整个业务实例进入最终状态

阶段结束负责阶段结算,流程结束负责最终收口。两层判断各自负责一层生命周期,不再混在一起。


六、结束之后,哪些状态保留,哪些状态清理

1. 阶段胜利:保留跨阶段状态,清理阶段内状态

拆分生命周期之后,很重要的一点是:

不同层级的结束,应该触发不同的后续动作。

阶段结束时,可能要做:

  • 生成当前阶段结算
  • 返回当前阶段结果
  • 准备下一阶段配置
  • 重置当前阶段临时状态
  • 保留跨阶段进度

整体流程结束时,可能要做:

  • 返回最终结果
  • 清理运行时状态
  • 释放临时资源
  • 持久化最终记录
  • 禁止后续继续操作

这两类动作明显不一样。

可以整理成表格:

场景阶段是否结束流程是否结束应该保留什么应该清理什么
阶段未结束当前运行时状态通常不清理
阶段胜利,流程继续用户资源、跨阶段进度、后续阶段配置当前阶段分数、阶段内临时状态
阶段失败,流程结束最终结果、日志记录运行时状态、临时效果
重新初始化可选历史记录旧运行时状态

比如当前阶段胜利后,是:当前阶段结束 -> 整体流程继续

这时候不能直接清空所有状态,因为用户还要进入下一阶段。

所以应该保留用户资源、当前进度、后续阶段配置,以及可能跨阶段生效的状态。但当前阶段得分、当前阶段临时计数、本阶段内的临时状态,就可以根据规则重置或清理。

2. 阶段失败:进入整体结束,清理运行时状态

而当前阶段失败后,是:当前阶段结束 -> 整体流程结束

这时候就可能需要清理运行时状态,禁止后续继续操作,并返回最终失败结果。

所以生命周期设计不是只判断true / false,而是要决定状态的保留和清理策略。

这时候继续保留局内运行时状态,反而可能影响后续重开或恢复逻辑。

3. 每次结束后都要问的几个问题

实际设计时,我会倾向于在每次“结束”后问这几个问题:

判断问题目的
当前结束的是操作、阶段,还是整体流程?先确定生命周期层级
结束后是否还会进入下一阶段?判断是否保留跨阶段状态
哪些状态只在当前阶段有效?判断需要清理哪些临时状态
哪些状态需要带到后续流程?判断需要保留哪些长期状态
前端下一步应该展示什么页面?判断返回结构应该带哪些字段
后续请求是否还允许继续执行?判断是否进入最终结束状态

七、通用后端里也很常见

这个问题其实不只存在于游戏后端。很多业务系统都有“阶段结束不等于流程结束”的情况。

这些场景的共同点是:某个节点完成了,但整个业务实例还没有结束。如果只用一个finished字段,就很容易把“节点完成”和“流程完成”混在一起。

业务场景小阶段结束整体流程结束如果混成一个状态会怎样
订单系统支付成功、发货完成订单完成 / 取消支付成功可能被误认为订单结束
审批系统当前节点通过整个审批通过 / 驳回一审通过可能被误认为流程完成
活动任务单个任务完成整个活动完成 / 过期任务完成可能被误认为活动结束
游戏后端当前 Blind 结束整局游戏失败 / 最终完成阶段胜利可能被误认为游戏结束

1. 订单系统:支付完成不等于订单完成

订单支付成功,只代表支付阶段结束,并不代表订单整体完成。

更清楚的拆法是:

状态含义
paymentStatus支付是否完成
deliveryStatus发货是否完成
orderStatus订单整体是否完成

如果只用一个finished,就很容易把“支付成功”和“订单完成”混在一起。

2. 审批系统:一审通过不等于审批完成

一个审批节点结束,不代表整个审批流程结束。

比如:提交申请 -> 一审通过 -> 二审通过 -> 最终通过

一审通过只是当前节点结束,整体审批流程还要继续往下走。

所以这里也应该区分:

状态含义
currentNodeStatus当前审批节点状态
approvalStatus整体审批流程状态
nextNode下一审批节点

3. 活动任务:单个任务完成不等于整个活动结束

一个任务完成,不代表整个活动结束。

比如:完成任务 A -> 领取奖励 A -> 解锁任务 B -> 完成整个活动

这里至少有三类状态:

  • 单个任务状态
  • 奖励领取状态
  • 活动整体状态

如果全部只靠一个finished,后面肯定会混乱。

所以本质上,这类问题都可以归纳成一句话:

节点完成,不等于流程完成。


八、状态命名要表达层级

1. 太泛的命名会被复用成多种含义

这次复盘里,我觉得还有一个点挺重要:

状态字段的命名,应该能表达它属于哪一层生命周期。

比如:

  • gameOver:表达的是整体游戏是否结束。
  • blindOver:表达的是当前Blind阶段是否结束。

如果是通用系统,也可以类似使用taskOverprocessOver,或者stageCompletedflowCompleted

命名不一定非要固定,但一定要避免一个字段表达多层含义。

可以简单对比一下:

不太清楚的命名问题更清楚的命名
finished不知道是任务结束、阶段结束,还是流程结束stageFinished/flowFinished
over语义太泛,容易被复用taskOver/processOver
status不知道是哪类状态paymentStatus/orderStatus
done容易变成万能完成标记nodeDone/workflowDone

2. 字段名要带上生命周期上下文

如果字段名太泛,很容易后面被复用成各种含义。

比如一个finished,刚开始可能表示当前任务结束,后面又被拿来表示整个流程结束,再后面又用来表示奖励已领取。

最后代码里到处都是:

if(finished){// ...}

但没人敢确定这个finished到底代表什么。

所以我现在会更倾向于让字段名多一点上下文。

不是为了啰嗦,而是为了减少误解。


九、不要急着上复杂状态机,但要先分清层级

1. 简单流程,一个 status 就够用

这里也需要注意一点。

我不是说一开始就必须上复杂状态机。如果系统还很简单,只有一个开始和结束,那一个status完全够用。

比如:status: "pending" | "finished";没有问题。

2. 多层流程出现后,就不能再硬扛

真正需要拆分生命周期,是在系统开始出现多层流程时。

比如:

  • 当前操作结束后,流程还会继续
  • 当前阶段结束后,还有下一个阶段
  • 某些状态需要跨阶段保留
  • 某些状态只在当前阶段有效
  • 不同结束原因触发不同处理动作

这时候才需要拆。

如果把具体业务抽掉,我现在会更倾向于这样理解:

生命周期层级结束条件示例影响什么
操作生命周期当前请求处理完成本次请求返回成功还是失败
阶段生命周期当前阶段目标达成或失败是否生成阶段结算、是否进入下一阶段
整体流程生命周期所有阶段完成或发生最终失败是否清理运行时状态、是否禁止继续操作

也就是说,后端在处理请求时,不只是返回“成功 / 失败”,而是要判断:

请求结束后要判断的问题对应生命周期
这次操作是否成功?操作生命周期
当前阶段是否结束?阶段生命周期
整体流程是否结束?整体流程生命周期

这三个问题都可能有不同答案。

3. 要避免两个极端

所以我的理解是:

不要为了架构而提前设计复杂状态机,但也不要在多层流程已经出现后,还用一个状态字段硬扛。

这里要避免两个极端:

  • 一个是系统还很简单时,就提前设计一套复杂状态机;
  • 另一个是多层流程已经出现了,还继续用一个finishedgameOver表达所有结束状态。

真正要做的,是在复杂度出现时及时拆分,而不是一开始就把所有可能性都设计出来。这中间其实是一个平衡。


十、本篇小结

这一篇主要复盘了一个问题:

为什么阶段结束不等于流程结束?

我的理解是:

当系统里出现多层流程时,一个“结束状态”往往不够用。因为当前阶段完成,只代表一个节点结束,不代表整个业务流程结束。

可以把这次复盘得到的结论整理成下面几条:

设计结论对应含义
不是所有“结束”都是同一种结束操作结束、阶段结束、流程结束要分开看
生命周期拆分的关键是后续动作不同如果结束后处理方式不同,就不该混成一个状态
状态字段要表达层级stageOverover更清楚
生命周期设计会影响状态清理清理哪些状态,取决于结束的是哪一层生命周期
不要过早复杂化简单流程可以简单处理,多层流程出现后再拆

如果用一句话总结:

生命周期状态要按层级拆分,不同层级的结束,应该触发不同的后续动作。

在后端设计里,很多状态问题不是字段本身复杂,而是字段背后的生命周期没分清楚。

如果生命周期层级清楚,后面的状态保留、状态清理、流程推进、返回结构,都会更自然。

下一篇会继续复盘:

业务配置归属设计:配置不是常量,放错位置后面会很痛。


🃏关于这个项目

本复盘专栏的内容,来自我正在实现的一个 Balatro 风格实时游戏后端项目。

主线系列《实时游戏后端工程实践:从 Balatro 出发》会记录项目从 0 到 1 的实现过程;而本专栏会从项目里抽出一些更通用的后端设计问题,把具体功能背后的设计取舍讲清楚。

如果你对状态设计、接口拆分、规则系统、奖励系统和游戏后端工程实践感兴趣,也可以顺着主线系列继续看。

http://www.jsqmd.com/news/1103419/

相关文章:

  • 收藏!小白也能学!2026年AI大模型应用开发工程师高薪转型指南
  • 【观止·诗史汇 HarmonyOS 实战系列 08】古今地理:从历史地名到诗文、事件、朝代的空间关联
  • 魔兽争霸III终极优化指南:如何在现代系统上完美运行经典游戏
  • 2026企业GEO选型指南:三大主流排名监测平台实战对比
  • 2026年Ozon ERP软件实测:爆单AI、妙手ERP、上品帮到底谁好用?个人卖家真实对比
  • 第二届通信网络与智能系统工程国际会议(ICCNSE 2026)成功在线举办
  • STM32与DS28EC20 EEPROM的嵌入式存储方案实践
  • 3分钟让你的网易云音乐在任何设备自由播放:ncmdumpGUI轻松解锁NCM格式
  • 【毕业设计】桂林旅游景点导游平台 SpringBoot+Vue 完整源码(含论文+数据库,可运行)
  • IntelliJ IDEA SQL控制台导出不生效?3分钟定位是IDE缓存、驱动版本还是JVM参数问题(含诊断树图)
  • 【自编工具】文件整理工具:自动解压压缩包 + 全局去重
  • 最后一次刷卡——替不会说话的东西办退休
  • 国网项目验收必看:功能、非功能、安全、渗透测试一站式办理指南!
  • 5分钟拯救B站收藏:如何用开源工具实现m4s视频永久备份?
  • 第一章Netty,ByteBuffer大小分配问题
  • 哪有什么免费的午餐?阿贝云免费主机入坑指南
  • ICM-42688-P与STM32L021K4在运动控制与工业监测中的应用
  • Smithbox免费开源游戏修改工具:魂系游戏Mod制作的终极指南
  • 如何快速搭建网易云音乐API服务:终极配置与开发指南
  • AMD Ryzen处理器免费调试神器:5分钟学会SMU Debug Tool完整指南
  • ncmdumpGUI:免费解锁网易云音乐加密NCM文件的终极Windows图形界面解决方案
  • DouyinLiveRecorder:一站式多平台直播录制解决方案,支持40+平台自动录制
  • Windows 10 环境下 Docker 部署 Sub2API 完整教程(避坑版)
  • 解决一个操作系统两个Java版本的问题
  • GPT 应用场景全解析:从代码编写到技术文档,AI 到底能帮你做什么?
  • 终极指南:如何轻松实现Switch与WiiU《塞尔达传说》存档自由转换
  • 三步掌握pywencai:Python高效获取同花顺问财数据的实战指南
  • BurpSuite API发现插件实战:自动化侦察与越权漏洞挖掘
  • GNSS定位与LTE Cat 1的嵌入式硬件实现方案
  • 2026 程序员 AI 兵器谱:Cursor vs GitHub Copilot vs 通义灵码 vs CodeBuddy 深度横评