← 返回文章列表

Dify 企业级实验(09):复杂业务状态机——订单状态流转与非法跳转防护?

1. 业务场景

先讲一个我们实际遇到的场景。

订单系统的状态流转,是「复杂业务系统」与「简单演示应用」的分水岭:待支付 → 已支付 → 已发货 → 已完成 / 已取消,还有一条售后链路。一次「未支付直接发货」,就能把业务数据搞乱。

我们第一次接这类需求时,第一反应也是「改个状态字段而已,有什么难的」。真正动手才发现——状态不是随便改的:每一步流转都要先问「当前状态允许走到哪」,答错了订单、工单、审批、售后全线崩坏。散落的 if 判断只会让规则越来越不一致,必须有一张迁移表做唯一真相。

这不是个例。任何「有明确状态流转的业务」都是这个模式:订单、工单、审批、售后,状态错了业务就乱了。

2. 场景痛点

这个流程的痛点,在订单系统和运维身上体现得最直接:

本质上,没有状态机的系统,一次非法跳转就能把业务数据搞乱——状态流转必须集中校验、全程可追溯

3. 方案:为什么是迁移表唯一真相

Dify 的 code 节点 + 外部 KV 存储,足以实现一张「迁移表唯一真相」的状态机。选它的理由:

这篇文章我们就用它搭一个「订单状态机」:迁移表校验 + 合法流转 + 变更日志。

4. 整体架构

graph TD start["开始:order_id/target_status/operator"] http_orders["http_orders:读取订单状态(GET KV 存储)"] cd_read["cd_read:解析当前状态"] cd_check["cd_check:迁移表校验(合法/非法)"] if5{"if5:迁移是否合法"} cd_transit["cd_transit:执行流转 + 组装变更日志"] http_order["http_order:更新状态(upsert)"] http_log["http_log:追加变更日志(append)"] cd_ok["cd_ok:组装结果"] end_ok["end_ok"] end_reject["end_reject:返回拒绝 + 当前状态 + 允许流转"] start --> http_orders --> cd_read --> cd_check --> if5 if5 -- "ok" --> cd_transit --> http_order --> http_log --> cd_ok --> end_ok if5 -- "false" --> end_reject

链路很清晰:读当前状态 → 迁移表校验 → 合法则流转并记日志 / 非法则拒绝并提示允许流转。集中校验是状态机的关键设计。

5. 模块设计

5.1 开始节点变量

- variable: order_id      # 文本,必填,订单号

- variable: target_status # 下拉,必填:已支付/已取消/已发货/已完成/售后中/售后完成

- variable: operator      # 文本,必填,操作人

5.2 迁移表校验 cd_check——唯一真相

所有流转校验都查这一张 dict,禁止散落的 if 判断(状态机核心):

def main(current_status: str, target_status: str) -> dict:

    transitions = {

        "待支付": ["已支付", "已取消"],

        "已支付": ["已发货", "已取消"],

        "已发货": ["已完成", "售后中"],

        "已完成": ["售后中"],

        "售后中": ["售后完成"],

        "已取消": [],     # 终态

        "售后完成": [],   # 终态

    }

    allowed = transitions.get(current_status, [])

    valid = "true" if target_status in allowed else "false"

    tip = "、".join(allowed) if allowed else "(终态,不可流转)"

    return {"valid": valid, "allowed": tip,

            "message": "当前状态:" + str(current_status or "") + ",允许流转:" + tip}

注意 valid 返回字符串 "true"/"false"(boolean 类型在变量选择器里不可见),if5 用 cd_check.valid is "true" 判断。

5.3 流转与变更日志 cd_transit

合法分支一次性产出两个载荷:状态更新(upsert 按 order_id 覆盖)与日志(append 只追加):

def main(order_id: str, current_status: str, target_status: str, operator: str) -> dict:

    import json, time

    now = time.strftime("%Y-%m-%d %H:%M:%S")

    order_item = {"order_id": order_id or "", "status": target_status or "", "time": now}

    log_item = {"order_id": order_id or "", "from": current_status or "", "to": target_status or "",

                "operator": operator or "", "time": now}

    return {"order_payload": json.dumps({"op": "upsert", "match_key": "order_id", "item": order_item}, ensure_ascii=False),

            "log_payload": json.dumps({"op": "append", "item": log_item}, ensure_ascii=False),

            "message": "订单 " + str(order_id or "") + " 已从「" + str(current_status or "") + "」流转到「" + str(target_status or "") + "」(操作人:" + str(operator or "") + ")"}

5.4 状态存储:KV 模拟服务

Dify code 节点运行在沙箱中禁止写文件(/tmp PermissionError),本实验用本机 KV 模拟服务(docker 容器 dify104-kv,172.19.0.50:8123)存状态与日志,生产替换为 Redis/DB,工作流拓扑不变:

http_orders: GET  http://172.19.0.50:8123/state/dify104_09_orders

http_order:  POST http://172.19.0.50:8123/state/dify104_09_orders   # body ← cd_transit.order_payload

http_log:    POST http://172.19.0.50:8123/state/dify104_09_log      # body ← cd_transit.log_payload

6. 运行验证

输入(order_id / target_status) 预期 实测
O1001 / 已支付 待支付→已支付 合法,流转成功,日志 +1 与预期一致,返回流转成功
O1002 / 已发货 待支付→已发货 非法,拒绝并提示允许流转 与预期一致,返回「当前状态:待支付,允许流转:已支付、已取消」
O1003 / 已发货 已支付→已发货 合法,流转成功 与预期一致
O1004 / 已完成 已发货→已完成 合法,流转成功 与预期一致
O1005 / 售后中 已完成→售后中 合法,进入售后链路 与预期一致
O1006 / 已支付 已取消是终态,拒绝(终态不可流转) 与预期一致,提示「(终态,不可流转)」

变更日志共 5 条,只追加(append),每条含订单号/前后状态/操作人/时间,可完整追溯。

7. 实战坑

现象 修复
code 节点沙箱禁写文件 写 /tmp 直接 PermissionError,文件模拟存储全不可行 改 http 节点 + 本机 KV 模拟服务(172.19.0.50:8123),生产换 Redis/DB(实测)
状态判断散落各处 规则不一致、漏校验,出现「未支付直接发货」 迁移表 dict 唯一真相,cd_check 统一校验(实测)
校验与更新分离无保护 并发下重复流转(竞态) 标注生产环境需事务/锁/乐观版本号(实验文档设计约束)
变更日志覆盖写 审计记录丢失,无法追溯 KV append 只追加,日志不可变(实测)
终态继续流转 已取消订单又被改回已支付,业务数据错乱 迁移表终态列为空列表,一律拦截(实测)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过