Home
avatar

.𝙃𝙖𝙣

状态一多,接口为什么就开始变复杂了

这篇算是把前面那几篇任务调度、异常降级、日志权限的收拢篇把。

很多时候接口变复杂,表面上看像是参数变多了、判断变多了、页面要求变多了。真把业务往后做一段时间,会发现很多复杂度其实都长在状态上。状态一开始看起来只是几个字段,后来会慢慢变成整个系统动作边界的承载点。到这个阶段,接口到底能不能继续收得住,很大程度取决于状态有没有先被定义清楚。

1. 一开始看状态字段,很容易觉得它只是个辅助信息

刚开始做接口时,状态字段通常不会显得特别“重”。

最直观的理解大概都是这样:

  • 0 是待处理
  • 1 是处理中
  • 2 是成功
  • 3 是失败

看起来很普通。

很多时候人也会本能地把它当成:

  • 页面要显示的文案来源
  • 列表筛选条件
  • 一个顺手补上的字段

在功能少、链路短的时候,这种理解勉强也能用。

因为这时候动作不多,状态也不多,接口里写几句判断就过去了。

但项目只要稍微往后推一点,状态就会开始膨胀。

比如 vchoo 这种链路里,很快就会碰到:

  • 创作任务状态
  • 故事生成状态
  • 分镜拆分状态
  • 生图子任务状态
  • 回调回写状态
  • 取消状态
  • 重试状态
  • 部分成功 / 部分失败状态

到了这个时候,状态就不再只是页面显示用的字段了。

它开始承担一件更核心的事:

系统当前允许做什么,不允许做什么。

这也是我后来对状态理解变化最大的地方。

2. 接口后来会变复杂,很多时候是因为动作都带上了前置状态

这是我后来一个很强烈的感受。

刚开始做接口时,很容易把接口理解成一个动作入口:

  • 创建
  • 修改
  • 删除
  • 重试
  • 取消

动作看起来都很直接。

但一旦业务链长起来,这些动作都会开始带前置条件。

比如“取消任务”这件事,表面看就是点一下按钮。

真进系统以后,你就得先判断:

  • 当前任务是不是还没执行
  • 当前任务是不是已经在执行中
  • 当前任务是不是已经成功
  • 当前任务是不是已经失败
  • 当前任务是不是已经取消过了

动作没有变,但动作能不能执行,开始被状态限制住了。

一旦这种限制变多,接口复杂度就会跟着上来。

因为 Controller、Service 甚至回调处理里都会不断出现类似问题:

  • 这个状态下允不允许改
  • 那个状态下还能不能重试
  • 已取消的任务回调回来后要不要更新结果
  • 部分成功时整体状态怎么算

所以接口越往后越重,通常不是代码突然写差了。更多时候,是业务动作开始被状态卡住了:

业务动作开始需要被状态约束。

3. 状态一多,先变重的往往是后端判断链

前端当然会感知到状态复杂。

比如:

  • 按钮什么时候禁用
  • 页面显示什么提示词
  • 当前卡片展示哪个标签

但真正先被状态拖复杂的,其实通常是后端。

前端更多是在展示,后端则要负责拍板。

比如一个看起来普通的接口,后端可能要先走完这样一串判断:

  • 主任务当前在什么状态
  • 子任务当前在什么状态
  • 上一步有没有完成
  • 这次动作会不会和当前状态冲突
  • 这个状态是不是已经被别的异步动作改过了
  • 当前这个写入会不会覆盖掉更晚的真实结果

只要这类判断一多,接口很快就不再是“收参数、调服务、返回结果”这么平了。

它会慢慢长出很多分支,而这些分支如果没有一个统一的状态理解,就会显得特别乱。

4. 状态不是展示字段,它本质上是在描述业务约束

这个点是我后来比较认同的一层理解。

因为只把状态看成展示字段,后面很多设计都会歪。

比如:

  • 页面想显示“处理中”,那就加一个状态
  • 页面想显示“已完成”,那就再补一个状态
  • 页面想区分“部分成功”,那就继续加一个状态

这样做的问题是,状态会越来越像页面文案表,而不是业务规则表。

可系统真正在意的,根本不只是“叫什么名字”,而是:

  • 这个状态下还允不允许做某个动作
  • 状态能不能从 A 直接跳到 C
  • 这个变化是不是必须经过中间步骤
  • 某个状态失败后,下一步到底是回退、重试还是终止

所以我后来会更愿意把状态理解成:

状态字段是在描述业务对象当前所处的约束区间。

这个对象现在能做什么、不能做什么,都得靠它来收。

一旦这样想,状态设计和接口设计就会自动绑在一起。

5. 状态一多以后,最难收的其实是边界

很多人一谈状态复杂,第一反应是状态太多不好记。

我现在回头看,记不住状态名字其实还不是最麻烦的。

真正难的是:

哪些状态属于主任务,哪些状态属于子任务,哪些状态只是某一层执行过程里的临时状态。

如果这一层不拆清楚,后面状态会相互污染。

比如在 vchoo 这种链路里:

  • 创作任务有整体状态
  • 分镜有自己的状态
  • 生图任务又有自己的状态

如果你把这些状态全揉在一处,后面很容易出现:

  • 主任务状态和子任务状态打架
  • 前端查整体进度时拿到的是局部状态
  • 某个分镜失败以后,整次任务立刻被打成失败
  • 某次局部重试把主状态覆盖乱了

所以我后来会越来越强调一件事:

状态要先分层,再分值。

先确定是谁的状态,再去定义它有哪些值。

这个顺序很重要。

6. 状态流转一旦没有规则,接口就会慢慢变成一堆 if else

这点其实在很多系统里都会发生。

一开始功能少的时候,写几个判断并不明显:

if (task.Status == Pending) { ... }
if (task.Status == Running) { ... }

这种写法前期完全能接受。

问题是状态一多、动作一多、异步一多,这种判断会开始成倍增长。

后面你会在很多地方看到类似逻辑:

  • Controller 里判断一次
  • Service 里再判断一次
  • 回调里再补一次
  • 重试时又判断一轮
  • 取消时再加一套分支

最后代码里到处都是状态相关的 if else。

复杂度真正往上窜,倒不是因为多写了几个 if else。问题在于:

状态流转规则没有被收住。

每个地方都在自己理解“当前状态还能不能做这件事”,系统就会越来越难维护。

所以我后来会更认同这样的方向:

  • 状态流转要有明确规则
  • 规则尽量往业务对象或领域层收
  • Controller 和外围层尽量别各写一套自己的状态判断

不然接口最终会复杂到很难改。

7. 回调、重试、取消这些动作一进来,状态设计就从“静态标记”变成“动态协商”

这也是我在任务系统里感受特别明显的一点。

如果系统没有异步任务,很多状态变化是顺序单线程的,问题还没那么明显。

但一旦有了:

  • 回调
  • 重试
  • 取消
  • 轮询查询

状态就不再只是单个地方自己改一下那么简单了。

因为这几类动作可能发生在不同时间点,而且都想去写同一个对象的状态。

比如:

  • 用户刚点了取消
  • 后端把状态标成已取消
  • 结果底层回调晚了一点回来
  • 回调又想把状态改成成功

这时候大家争的已经不是字段名了,真正要定的是:

谁有资格在什么时候覆盖哪个状态。

所以从这个阶段开始,状态设计已经和并发、异步、最终一致性这些问题直接连上了。

也正因为这样,接口复杂度会迅速放大。

因为后端在处理接口时,不只是做业务动作,还得处理状态写入时机和覆盖顺序。

8. 我后来会把“状态复杂”拆成三层来看,这样脑子会清楚很多

这也是我后来给自己留下的一个比较有用的看法。

状态一多以后,如果全堆在一起看,很容易乱。

我现在会更倾向于拆成三层:

第一层:业务阶段状态

比如:

  • 草稿
  • 待执行
  • 执行中
  • 已完成
  • 已失败
  • 已取消

这一层最贴近业务动作。

第二层:执行过程状态

比如:

  • 已入队
  • 已下发
  • 回调处理中
  • 已落库

这一层更像执行链内部状态。

第三层:展示状态

比如前端最终显示的:

  • 处理中
  • 部分完成
  • 可重试
  • 已终止

这三层如果不区分,后面很容易出现一个字段既想表达业务意义,又想表达执行细节,还想直接给前端拿去显示。

结果就是谁看都别扭。

这个拆法对我帮助很大。

至少它让我知道:

  • 不是所有状态都该塞进一个字段
  • 不是所有状态都该直接暴露给前端
  • 不是所有状态都该用来做业务判断

9. 回头看,很多接口之所以越写越重,是状态一直没被真正建模

我现在对这件事的一个判断是:

很多接口之所以越写越重,不一定是因为写代码的人水平不够,也不一定只是分层没分好。

更多时候,问题根源在于:

状态还只是字段,还没有真正变成业务模型的一部分。

一旦状态还没建模,后面所有动作都只能在外围层补判断。

  • 页面补一层
  • 接口补一层
  • Service 补一层
  • 回调再补一层

每一层都在试图理解“现在应该怎么办”,系统自然越来越复杂。

所以我后来会越来越觉得,状态设计其实很像架构设计的一部分。

它看着像字段,实际上在决定几件更底层的事:

  • 业务对象怎么变化
  • 接口怎么控制动作边界
  • 失败以后系统怎么继续往下走

10. 结论

  1. 状态一开始看着像辅助字段,后面往往会变成业务约束的承载点
  2. 接口会变复杂,常常是因为动作开始受状态约束
  3. 状态要先分层,再定义取值,不然主任务和子任务很容易互相污染
  4. 状态流转规则如果没收住,接口代码很快就会长成一片 if else
  5. 回调、取消、重试一进来,状态问题就会直接变成一致性问题

这篇先记到这里。

.NET 状态流转 接口设计 后端 业务复盘