后端复盘(4):阶段结束不等于流程结束,一个 finished 字段为什么不够用
本文来自《后端系统设计复盘从游戏项目到通用后端》专栏。文章内容源于我正在实现的一个 Balatro 风格实时游戏后端项目但本文不会重点讲某个具体游戏功能怎么写而是抽出一个更通用的后端设计问题当一个系统里同时存在“小阶段结束”和“大流程结束”时为什么不能只用一个状态字段硬扛。这个问题不只存在于游戏后端在订单系统、审批流程、活动任务、工作流系统里也很常见。很多状态混乱不是因为代码写错了而是因为一个字段被迫表达了太多层含义。⚠️ 比如finished true到底表示什么当前操作完成了当前阶段结束了整个流程结束了只是进入了结算页如果这些问题需要靠上下文猜那这个状态字段就已经开始失控了。这篇文章想表达的核心观点是当系统里同时存在“当前阶段结束”和“整体流程结束”时不能只用一个gameOver、finished或done来硬扛。不同层级的结束往往代表不同的后续动作有的要结算有的要推进有的要保留状态有的要清理状态。生命周期拆分的重点不是多加几个字段而是让系统知道当前结束的是哪一层流程以及接下来应该做什么。文章目录一、问题是怎么出现的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. 早期只有一层结束状态在项目早期流程还比较简单所以我当时并没有把“结束状态”拆得很细。那时候系统更像是一个单局流程用户开始一局游戏然后出牌、弃牌、补牌、算分最后判断这一局是赢了还是输了。在这种复杂度下一个gameOver或gameStatus确实够用。比如gameOver: boolean;或者gameStatus: playing | finished;因为那个时候流程大概是这样的开始游戏 - 用户操作 - 判断是否结束 - 结束后返回结算整个系统只有一层生命周期当前这一局是否结束。2. 多阶段流程出现后问题开始暴露但后面继续往下做的时候流程不再只有一层“结束”这个词开始出现不同含义。因为系统开始不只是“一局打完就结束”而是出现了多个阶段。比如small blind - big blind - boss blind - next ante这时候某一个阶段结束并不代表整个流程结束。用户通过当前阶段后应该进入下一个阶段。只有用户失败或者整个大流程达到最终条件时整体流程才真正结束。也就是说系统里开始出现了两种“结束”当前阶段结束整体流程结束这个时候如果还只用一个gameOver就会开始变得很别扭。因为它已经表达不了真实业务含义了。3. 为什么这不是游戏特有问题虽然这个问题是在游戏流程里暴露出来的但它并不是游戏后端特有的问题。只要一个系统里存在“节点推进”或“阶段流转”就很容易出现类似情况某个小阶段已经完成但整个业务流程还没有结束。比如订单系统里支付完成不等于订单完成审批系统里一审通过不等于整个审批通过活动任务里单个任务完成也不等于整个活动结束。二、为什么一个 finished 字段不够用1. 它表达不了“阶段结束但流程未结束”这里的gameOver只是当前项目里的字段名。放到更通用的系统里它可能叫finished、done、completed或processOver。一开始只有gameOver时它的含义很清楚游戏是否结束。但当系统开始出现阶段推进后gameOver就开始有点尴尬了。比如当前阶段通过了这个时候当前阶段确实结束了但整场流程并没有结束。如果把gameOver设成true前端可能会以为整场游戏结束了如果把gameOver设成false又表达不出当前阶段已经结算完成。这就说明一个问题一个状态字段正在被迫表达两层含义。2. 代码会开始依赖组合判断这种状态很危险因为它会让代码开始出现很多奇怪判断。比如if(gameOverresultWIN){// 其实不是整场结束只是当前阶段结束}if(gameOverresultLOSE){// 这次才是真的整场结束}看起来能写但语义已经开始混了。真正的问题不是代码能不能跑而是字段名字和业务含义已经对不上了。3. 前端和服务端都会开始猜语义如果继续用一个gameOver硬扛至少会带来三个问题问题具体表现前端不知道展示什么gameOver true到底是阶段结算页、失败页还是最终结束页服务端推进逻辑混乱阶段胜利要推进阶段失败要清理但字段表达不出来后续扩展越来越痛加奖励、商店、临时效果时只能继续补判断比如gameOver true时前端到底应该展示当前阶段结算页、整场失败页、下一阶段入口还是最终结束页如果还要再看result、reward、nextStage等字段才能猜出来那前端逻辑就会越来越依赖隐含规则。服务端自己也会遇到类似问题。当前阶段结束后有些情况要保留状态有些情况要清理状态。如果只用一个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 trueWIN可能只是当前阶段通过finished trueLOSE可能才是整体流程结束字段本身没有表达清楚状态层级只能靠组合判断补语义。更清楚的写法是typeProgress{stageOver:boolean;flowOver:boolean;result?:WIN|LOSE;};这样前端和服务端都能直接知道当前结束的是阶段还是整个流程。五、拆分前后流程判断有什么变化1. 拆分前一个 gameOver 承载所有结束含义拆分之前流程可能是这样的falsetrue用户操作计算结果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阶段是否结束。如果是通用系统也可以类似使用taskOver、processOver或者stageCompleted、flowCompleted。命名不一定非要固定但一定要避免一个字段表达多层含义。可以简单对比一下不太清楚的命名问题更清楚的命名finished不知道是任务结束、阶段结束还是流程结束stageFinished/flowFinishedover语义太泛容易被复用taskOver/processOverstatus不知道是哪类状态paymentStatus/orderStatusdone容易变成万能完成标记nodeDone/workflowDone2. 字段名要带上生命周期上下文如果字段名太泛很容易后面被复用成各种含义。比如一个finished刚开始可能表示当前任务结束后面又被拿来表示整个流程结束再后面又用来表示奖励已领取。最后代码里到处都是if(finished){// ...}但没人敢确定这个finished到底代表什么。所以我现在会更倾向于让字段名多一点上下文。不是为了啰嗦而是为了减少误解。九、不要急着上复杂状态机但要先分清层级1. 简单流程一个 status 就够用这里也需要注意一点。我不是说一开始就必须上复杂状态机。如果系统还很简单只有一个开始和结束那一个status完全够用。比如status: pending | finished;没有问题。2. 多层流程出现后就不能再硬扛真正需要拆分生命周期是在系统开始出现多层流程时。比如当前操作结束后流程还会继续当前阶段结束后还有下一个阶段某些状态需要跨阶段保留某些状态只在当前阶段有效不同结束原因触发不同处理动作这时候才需要拆。如果把具体业务抽掉我现在会更倾向于这样理解生命周期层级结束条件示例影响什么操作生命周期当前请求处理完成本次请求返回成功还是失败阶段生命周期当前阶段目标达成或失败是否生成阶段结算、是否进入下一阶段整体流程生命周期所有阶段完成或发生最终失败是否清理运行时状态、是否禁止继续操作也就是说后端在处理请求时不只是返回“成功 / 失败”而是要判断请求结束后要判断的问题对应生命周期这次操作是否成功操作生命周期当前阶段是否结束阶段生命周期整体流程是否结束整体流程生命周期这三个问题都可能有不同答案。3. 要避免两个极端所以我的理解是不要为了架构而提前设计复杂状态机但也不要在多层流程已经出现后还用一个状态字段硬扛。这里要避免两个极端一个是系统还很简单时就提前设计一套复杂状态机另一个是多层流程已经出现了还继续用一个finished或gameOver表达所有结束状态。真正要做的是在复杂度出现时及时拆分而不是一开始就把所有可能性都设计出来。这中间其实是一个平衡。十、本篇小结这一篇主要复盘了一个问题为什么阶段结束不等于流程结束我的理解是当系统里出现多层流程时一个“结束状态”往往不够用。因为当前阶段完成只代表一个节点结束不代表整个业务流程结束。可以把这次复盘得到的结论整理成下面几条设计结论对应含义不是所有“结束”都是同一种结束操作结束、阶段结束、流程结束要分开看生命周期拆分的关键是后续动作不同如果结束后处理方式不同就不该混成一个状态状态字段要表达层级stageOver比over更清楚生命周期设计会影响状态清理清理哪些状态取决于结束的是哪一层生命周期不要过早复杂化简单流程可以简单处理多层流程出现后再拆如果用一句话总结生命周期状态要按层级拆分不同层级的结束应该触发不同的后续动作。在后端设计里很多状态问题不是字段本身复杂而是字段背后的生命周期没分清楚。如果生命周期层级清楚后面的状态保留、状态清理、流程推进、返回结构都会更自然。下一篇会继续复盘业务配置归属设计配置不是常量放错位置后面会很痛。关于这个项目本复盘专栏的内容来自我正在实现的一个 Balatro 风格实时游戏后端项目。主线系列《实时游戏后端工程实践从 Balatro 出发》会记录项目从 0 到 1 的实现过程而本专栏会从项目里抽出一些更通用的后端设计问题把具体功能背后的设计取舍讲清楚。如果你对状态设计、接口拆分、规则系统、奖励系统和游戏后端工程实践感兴趣也可以顺着主线系列继续看。