资讯动态

轻量级规则执行引擎rea:统一事件驱动与任务调度的自动化运维实践

发布时间:2026/10/21 4:50:29 来源:尧图企业网站定制
这个项目叫rea全称Rule Execution Agent一个自研的轻量级规则执行引擎。先说清楚它是干什么的把分散在服务器上的定时任务、Webhook回调、消息队列事件全部收拢到一套统一规则体系里通过“事件进来→规则匹配→动作执行→结果记录”这样一条链路驱动替代过去到处乱丢的cron脚本和各自为战的任务代码。如果你现在还在用crontab加一堆没人敢动的Python脚本维护自动化或者团队里每个人各写一套事件处理逻辑那这篇文章值得你花十分钟看完。它最初是我在一个内部自动化平台项目里为了解决任务管理混乱而写的后来演化成了一个独立的引擎模块。这个项目不算复杂但踩过的坑不少尤其是规则命中、重试机制、幂等处理这些细节几乎每一个都能单独写一篇文章。我尽量把这套东西从设计思路到核心实现再到排障经验都讲透方便你直接参考或者在此基础上改造成自己的版本。1. 为什么我们要自研一个轻量级规则执行引擎1.1 项目背景脚本式任务编排有多痛苦团队当时维护一个内部自动化服务平台承接的工作包括每日报表生成、订单数据同步、库存告警联动、定时清理临时文件等听上去都是小事但真实推进起来非常难受。最初的做法很朴素每接一个需求就在服务器上加一条crontab再配一个独立的Python脚本或者十几行Shell命令。新需求来的时候简单写个脚本挂上去就行。但半年之后问题开始集中爆发。先说最直接的一个脚本位置混乱。大家的脚本分散在各台机器的不同目录有的放在/home/xxx/scripts/有的放在/opt/tools/还有的直接放在/tmp/下面。人员一流动谁也不知道哪些脚本还在跑、哪些已经废弃了服务器重启之后依赖环境没配好任务直接静默失败。其次是重试和处理结果不可控。一个批量同步脚本执行到一半因为数据库连接超时中断了脚本没有重试逻辑也没有把失败状态写到任何地方。等到第二天业务人员反馈“怎么昨天的数据一直没到”我们才开始翻日志找问题有时候一个简单问题要排查一两个小时。更麻烦的是事件类需求。比如某个平台的Webhook回调触发后需要更新业务状态或者监控系统告警后需要自动执行一个处理动作。这类需求没法用cron表达往往都是临时写个HTTP服务承载或者用消息队列手动消费。每个服务端口、鉴权方式、消费逻辑都不一样维护成本越来越大。这时候我们需要一个统一的东西不管任务是定时触发、Webhook触发还是消息到达触发都用同一种方式描述由同一个执行引擎统一调度、重试、记录结果。1.2 调研结论为什么没直接用开源规则引擎动手之前我们做过一轮技术调研看了几类现成方案。第一类是调度框架比如常见的分布式任务调度平台。这类系统对任务编排能力强但偏重“调度”对事件规则匹配、条件表达式、动作绑定的抽象较弱。我们很多场景是“当某个事件满足某个条件时就执行某个动作”这更像规则引擎的领域。直接拿调度框架来改要叠加很多自己的逻辑。第二类是企业级规则引擎功能确实强大支持复杂的DRL规则文件、决策表、工作流编排。但问题也很明显学习成本高、框架重、部署复杂。我们团队需要的是一个轻量级、能像普通Python库一样被集成和维护的组件不想给平台引入一个不知道有多少依赖的庞然大物。第三类是看起来最接近的“事件驱动框架”比如消息队列配合流处理框架的组合方案。优势是吞吐大、扩展性强但那是面向大规模流式处理场景的武器我们用起来属于杀鸡用牛刀。而且这类框架的处理模型偏“数据流”对“命中规则后执行一个有状态动作”的支持不够直接比如动作里要请求外部HTTP接口并等待返回值处理起来比较别扭。综合考虑后我们都倾向一个更务实的方向用一个Python编写的、带配置文件的、包含事件监听、规则匹配、动作执行、任务状态管理的小引擎。这正是rea的雏形。rea这个名字是后来起的Rule Execution Agent意思是“规则执行代理”。它不追求通用只做一件事把“事件到动作”的映射关系用声明式配置表达出来并且可靠地执行完。1.3 核心设计目标用配置消灭代码散乱做前期的目标拆解时我们把核心需求收敛成了四条。第一条是统一入口。不管是定时触发、HTTP回调、消息队列消费都必须经过同一条处理链路进入规则引擎不许再出现“每个需求一个单独服务”的情况。第二条是规则可配置。新加一个规则、修改一个动作地址、调整一个条件表达式都要通过改配置文件或者调用管理接口完成不需要重新发布代码。第三条是执行可观测。每个任务从触发、匹配到执行、完成或失败都要有日志和状态记录。出问题的时候能快速定位“这个动作为什么没执行”或者“为什么执行了两次”。第四条是失败可恢复。任务执行过程中进程重启了或者动作接口超时了系统要能通过重试和恢复机制继续处理而不是无声无息地丢掉。这四个目标最终成为rea的基本设计骨架。2. rea的架构设计与核心概念2.1 三大核心抽象规则、触发与动作rea把整个系统抽象成三个核心概念任务Job、规则Rule和动作Action。许多人第一次接触规则引擎会纠结于“事件”和“任务”的概念区别。在我这个设计里事件是一个瞬时发生的事实比如“收到一条Webhook”“到了某个cron时间点”“队列里来了一条新消息”。事件本身不存储状态处理完就没有了。动作则是一个可能耗时、可能失败、需要被追踪的操作比如“调用订单同步接口”“执行一个停机脚本”“发送一条企业微信通知”。因为动作有副作用所以必须被纳入任务管理。一条任务由一个触发条件加上多条规则构成。触发条件决定任务什么时候进入引擎规则决定任务是否被接受、要做什么动作。举个例子系统收到一条“订单创建”Webhook事件触发条件匹配到“订单事件”这个任务。任务里有多条规则其中一条规则的条件是“订单金额大于10000”对应的动作是“通知财务复核”另一条规则的条件是“订单状态为已付款”对应的动作是“调用库存锁定接口”。这样就把一条事件拆成了多个可能执行的路径。在具体实现上触发条件就是Trigger规则是包含了条件表达式和动作引用的Rule动作是具体执行器通常是HTTP调用、Shell命令、消息发送等类型。2.2 执行链路的四阶段状态机rea的执行链路是固定四段式接收事件、规则匹配、排队执行、记录结果。事件接收阶段由不同的Source负责。定时任务用CronSourceWebhook用HTTPSource消息队列用StreamSource。每个Source把外部事件统一成内部Event对象Event里有事件ID、事件类型、事件时间、业务数据payload和来源信息。规则匹配阶段由一个Matcher完成。Matcher根据事件类型找到关联的任务逐条执行规则中的条件表达式把命中的规则和对应的动作构造为一个待执行单元放入执行队列。排队执行阶段由Dispatcher驱动。Dispatcher维护一组Worker线程/进程每个Worker从队列里取一个待执行单元按动作配置执行并处理超时和重试。记录结果阶段由状态管理器负责。每一条任务从进入引擎开始就有一个状态流转整体是pending排队中→ running执行中→ success成功或者failed失败。失败的任务如果设置了重试次数会重新回到pending并等待下一次调度而不是直接丢弃。这个状态机在设计上参考了工作流引擎的思路但砍掉了复杂的流程编排只保留线性状态转移目的就是让定位问题足够简单。任何一条任务卡在哪里看一眼状态就知道。2.3 规则配置的DSL设计规则采用YAML编写因为YAML可读性强、支持注释、团队里几乎所有人都能看懂。一个完整的规则配置大致长这样task: id: order_sync_daily trigger: type: cron expr: 0 2 * * * timezone: Asia/Shanghai accept: - type: order.created - type: order.updated rules: - id: sync_to_erp when: payload.amount 100 and payload.env prod action: type: http method: POST url: http://erp.internal/api/order/sync headers: token: ${ERP_TOKEN} timeout: 10 max_retries: 3 retry_interval: 30 - id: notify_finance when: payload.amount 50000 action: type: notify channel: webhook target: https://hook.mysite.local/finance这里每个字段都是有意为之的。id是任务和规则的唯一标识后续所有日志和状态记录都用它关联。trigger.type决定用什么Source来接收expr是cron表达式或事件类型过滤。accept字段用于快速过滤事件类型相当于白名单能减少不必要进入规则引擎的数据。when字段是条件表达式用简单布尔表达式描述支持比较、逻辑与或非、成员判断。action是动作定义按类型区分执行器所有动作参数都在这一段配置。实际使用中DSL越简单越好。规则定义如果复杂到需要用户看半天文档接受度必然低。rea的DSL刻意控制在了上面这个规模覆盖了我们90%以上的场景。3. 核心实现从一个最小可用的引擎写起3.1 项目结构与环境依赖rea的代码量不大整个核心模块控制在两千行左右。项目结构如下rea/ ├── __init__.py ├── engine.py # 主引擎负责启动Source、调度、状态管理 ├── models.py # Event、Task、Rule、ActionResult等数据模型 ├── matcher.py # 规则匹配器 ├── dispatcher.py # 执行调度器管理Worker ├── actions/ │ ├── __init__.py │ ├── http_action.py │ ├── shell_action.py │ └── notify_action.py ├── sources/ │ ├── __init__.py │ ├── cron_source.py │ ├── http_source.py │ └── stream_source.py ├── config.yaml # 规则配置 └── main.py # 启动入口依赖做到尽可能少核心运行时只需要Python 3.9及以上。HTTP服务和消息队列支持分别通过可选的第三方库提供比如Web框架、Redis客户端但这样剥离之后最简情况下连第三方库都不用装。这种“核心零依赖”的设计不是刻意追求极简而是为了降低部署成本和排障复杂度。平台里其他服务用的技术栈五花八门如果rea本身引入一堆依赖和别的服务放在同一台机器上时容易冲突。单独跑在容器里的时候镜像也能做得很小。3.2 事件模型与条件表达式解析数据模型是整个引擎的基石。Event和Rule的定义如下from dataclasses import dataclass, field from typing import Any, Dict, Optional dataclass class Event: event_id: str event_type: str event_time: str payload: Dict[str, Any] source: str unknown dataclass class Rule: id: str when: Optional[str] None action: Dict[str, Any] field(default_factorydict) enabled: bool True这里的when是一个字符串表达式需要运行时解析。最开始我们考虑直接用Python的eval但eval在安全性上风险太高。规则配置来自内部平台风险相对可控但也不是没有漏洞的可能。比如某个规则写了一个恶意的表达式一旦触发就会执行任意代码。后来我们把条件表达式解析限制在一个安全子集里只允许访问payload字段、比较运算符、逻辑运算符、in和not in判断其他语法一律解析失败。用了一个约百行的递归下降解析器实现不依赖第三方库。虽然简单但足够满足业务需求。解析器核心逻辑大致是这样import ast import operator class ConditionEvaluator: def __init__(self, payload: dict): self.env {payload: payload} def evaluate(self, expression: str) - bool: try: tree ast.parse(expression, modeeval) return self._eval_node(tree.body) except SyntaxError: return False def _eval_node(self, node): if isinstance(node, ast.Compare): left self._eval_node(node.left) for op, comparator in zip(node.ops, node.comparators): right self._eval_node(comparator) if not self._apply_op(op, left, right): return False left right return True elif isinstance(node, ast.BoolOp): values [self._eval_node(v) for v in node.values] if isinstance(node.op, ast.And): return all(values) if isinstance(node.op, ast.Or): return any(values) elif isinstance(node, ast.Attribute): value self._eval_node(node.value) return value.get(node.attr) if isinstance(value, dict) else None elif isinstance(node, ast.Subscript): container self._eval_node(node.value) key node.slice.value return container.get(key) if isinstance(container, dict) else None elif isinstance(node, ast.Name): return self.env.get(node.id, {}) elif isinstance(node, ast.Constant): return node.value elif isinstance(node, ast.UnaryOp): value self._eval_node(node.operand) if isinstance(node.op, ast.Not): return not value return None这个实现的安全性在于整个解析过程基于抽象语法树遍历只支持预定义的节点类型遇到未支持的语法会返回False或者抛出异常不执行任何底层函数调用。3.3 调度器Worker池与重试逻辑Dispatcher是引擎里最容易出问题的地方它的职责是从内部队列取待执行单元交给Worker执行同时管理重试和超时。我实现了一个简单但足够可靠的调度器import queue import threading import time import traceback class Dispatcher: def __init__(self, worker_num4): self.queue queue.Queue() self.workers [] self.worker_num worker_num self.running False def start(self): self.running True for i in range(self.worker_num): t threading.Thread(targetself._worker_loop, namefworker-{i}) t.daemon True t.start() self.workers.append(t) def stop(self): self.running False # 等待当前任务结束 def submit(self, task): self.queue.put(task) def _worker_loop(self): while self.running: try: task self.queue.get(timeout1) except queue.Empty: continue try: self._execute_task(task) finally: self.queue.task_done() def _execute_task(self, task): rule task[rule] event task[event] action rule.action max_retries action.get(max_retries, 0) retry_interval action.get(retry_interval, 30) for attempt in range(max_retries 1): result do_action(action, event) if result.success: task_state success save_task_result(task, result) break else: if attempt max_retries: time.sleep(retry_interval) continue save_task_result(task, result, failedTrue)这里有几个细节值得展开。第一Worker数量默认设4不是拍脑袋定的。我们内部统计过绝大多数动作是HTTP调用和Shell命令耗时主要集中在网络IO上CPU占用很低。4个Worker已经能覆盖日常并发而且线程过多会带来不必要的上下文切换和文件描述符开销。如果动作负载更高可以加大Worker数量但要先看目标接口的承受能力不然Worker一多反而把对端打挂了。第二超时处理必须在do_action里实现。HTTP动作必须设置socket超时和整体读取超时Shell动作也要用timeout参数限制执行时间。否则一个卡死的接口就能让Worker一直占着不放等于整个调度器少了一个执行名额。第三重试之间给了一个sleep。这里虽然没有退避策略但retry_interval是必须有人负责设置的。曾经我们把重试间隔设成3秒结果对端数据库故障恢复很慢3秒一次重试把数据库完全压死了从故障变成了一场更大的事故。后来把重试间隔调成30秒以上并且配合最大重试次数限制对端才缓过气来。3.4 Source接入定时、HTTP与消息流Source是事件产生的入口不同Source复用同一套下游逻辑只需要把外部事件转换成统一的Event对象。CronSource是定时任务的核心用线程触发cron表达式。每次cron到达时间点构造一个Eventevent_type固定为cron.xxx。因为cron表达式本身的解析可以复用第三方库所以这块依赖是允许的。需要注意timezone字段默认用本地时区但最好显式指定一个时区避免服务器跨时区迁移时定时任务整体偏移。HTTPSource是一个内嵌的HTTP服务接收外部Webhook请求。接收的逻辑不复杂def handle_webhook(payload, headers): event Event( event_idgenerate_uuid(), event_typepayload.get(event_type, unknown), event_timepayload.get(timestamp, now_iso()), payloadpayload, sourcewebhook ) engine.submit_event(event) return {status: accepted},这里一个容易被忽视的问题Webhook请求需要快速返回接收方不应当在这里执行任何耗时操作而是把事件直接交给引擎。否则调用方等待时间过长会触发自己的超时和重试造成重复投递。StreamSource负责从Redis Stream或者消息队列消费。实现是轮询读取每取到一条消息解析后当作Event进入引擎。消费成功后才ack确保消息不丢。这里要注意的是如果规则匹配失败或者动作执行失败消息已经ack了那这个事件就找不回来了。所以要确认事件落地的可观测性否则排查问题时会很困难。4. 部署、配置与性能调优4.1 容器化部署与最小配置rea部署在客户端平台一个独立的容器里内存设置512MB足够CPU限制在1核即可。启动命令很简单python main.py --config config.yaml入口main.py负责加载配置、初始化引擎、注册Source、启动Dispatcher然后保持进程运行。环境变量的设计上所有敏感信息比如令牌、密钥、目标接口地址的端口部分统一走环境变量不在YAML里写明文。YAML里的占位符由引擎启动时做替换类似${ERP_TOKEN}这样的格式。还有一个容易踩的坑容器的时区。默认情况下Python的datetime.now()返回的是宿主机或容器配置的时区但很多业务配置依赖“本地时区0点开始跑”。如果容器时区是UTC那“0 2 * * *”会跑到北京时间上午10点。所以配置文件的timezone字段一定要显式设置不能依赖环境。我们在部署脚本里统一加了时区环境变量配合配置里显式声明双重保险。4.2 日志与可观测性设计可观测性是rea花功夫最多的地方之一。首先日志统一JSON格式输出。一行日志是一个JSON字段包括时间、事件ID、任务ID、规则ID、动作类型、状态、耗时、错误信息。结构化的好处是可以通过日志平台直接检索比如输入event_id就能拉出这条事件从进入引擎到执行完成的全链路日志。{timestamp:2025-01-15T10:00:00Z,level:INFO,event_id:ev_1234,task_id:order_sync_daily,rule_id:sync_to_erp,action_type:http,status:success,duration_ms:245}其次内部维护了一个简单指标计数包括每类事件的接收数、规则命中数、动作成功数、动作失败数、重试次数、队列深度。这些指标通过HTTP接口暴露供监控系统定期抓取。上线跑了一阵之后发现判断一个规则是否生效最直观的办法就是看命中计数曲线比翻日志高效得多。最后任务结果持久化到了平台MySQL数据库。表结构只有三张一张存任务定义、一张存规则定义、一张存执行记录。执行记录表是排查问题的依据保留60天自动清理过期的记录。4.3 压测数据与Worker数量调整上线前我们对rea做了一次简单的性能摸底数据供参考。场景模拟外部Webhook发送订单事件频率100个/秒每个事件匹配3条规则其中1条动作是HTTP调用对端模拟接口只做超短延迟返回另外2条在条件匹配阶段就被过滤掉。设置4个Worker运行10分钟。结果事件接收和处理没有积压队列深度稳定在个位数HTTP动作平均耗时约80ms其中50ms是对端接口的响应时间CPU使用率大约40%内存稳定在300MB左右。整体结论是当前场景下4个Worker是够用的。如果对端接口变慢比如响应时间到2秒同样的流量下队列就会明显积压。这时候有两个选择加大Worker数量或者把重试策略里的超时时间调短。我的建议是优先控制超时时间把单个动作的最长允许时间卡住避免慢接口拖垮整个调度系统而不是无脑加Worker。Worker加得再多对端响应不过来还是没用。5. 常见问题与排障实战5.1 规则没有触发先看命中数和事件类型上线接的第一个真实需求就遇到问题。业务方反馈某条Webhook事件进来了但对应的动作一直没有执行。排查路径很典型先在监控指标里查这条event_type的接收计数发现接收计数一直在涨说明事件确实进入了引擎再看命中规则计数发现是0。这就意味着事件在规则匹配阶段被拦截了。回头检查规则配置发现accept字段里写的是order.created但Webhook实际发送的event_type是order.create大小写和命名不一致。规则匹配的精确匹配在这里暴露了第一个坑。后来我们做了两个改进。一是规则匹配改成不区分大小写。二是给accept字段增加了一种后缀匹配模式允许配order.*。这样即使上游事件类型有小变化也不至于全部失配。5.2 任务重复执行幂等设计必须前置Webhook类事件有一个天然问题调用方有自己的重试机制一次事件可能被投递两遍甚至更多。如果引擎侧不做幂等处理一个动作可能被重复执行多次造成重复通知、重复扣减库存等事故。我们用的方案是事件ID去重。每个进入引擎的事件都有一个唯一event_id引擎在处理前先查Redis里的去重集合如果已经存在就直接丢弃。去重窗口设置成24小时保证跨天重试不重复。这里有一个细节动作执行成功和事件去重必须保证原子性拆分。如果先标记事件已处理再执行动作动作失败后就没有重试机会了如果先执行动作事件去重前进程崩溃重投后又会执行一次。我们的做法是事件去重标记在动作执行成功后统一提交之间加一个processing状态表示正在执行。这样既不会丢掉失败重试也不会无限重复。5.3 队列积压导致任务延迟执行某次大促期间业务流量比平时高了几倍Webhook事件大量涌入。监控显示队列深度一路飙升到数万很多任务延迟了十分钟以上才执行。排查后发现问题不在调度器而在于一个动作类型同步库存接口。大促高峰这个接口本身响应就很慢平均要3秒甚至超时。Worker执行一个任务要花很久新的任务就积压在队列里。处理方式分两步走。立即处理把动作的超时时间从10秒改成5秒对端接口超时后直接判定失败并进入重试队列不长时间占着Worker。同时临时把Worker数量从4调到8先把积压消化掉。长期优化对这类同步接口改成异步任务模式引擎只负责投递一个“待同步”消息到消息队列由下游的单独服务消费并执行同步rea只观察最终结果不做同步等待。这也是rea这套设计的一个优点动作执行器是可替换的。HTTP动作从同步调用切换成异步投递只需要改动作类型和配置不需要动引擎核心。5.4 内存缓慢增长与线程泄漏rea上线跑了几周发现内存占用从300MB缓慢涨到700MB重启后才恢复。一开始怀疑是Worker线程泄漏分析线程数量后发现并没有增加这说明问题出在数据积压。顺着排查发现事件去重的Redis集合在动作执行完成后会正常清理但任务执行记录在内存里有一个“最近执行结果”缓存用来给管理端提供实时状态展示没有设置过期时间。每条执行记录几KB每天丢几万条进去内存就涨上去了。修复方式很简单给缓存加了一个容量上限和LRU淘汰策略只保留最近1万条结果。同一周我们又做了一次代码审查把所有全局对象扫了一遍给所有队列和缓存都补齐了上限。内存终于稳定在350MB以下。这类问题其实很常见特别是对于常驻服务。内存释放、队列上限、缓存淘汰这些事上线前就得想清楚而不是等报警了再手忙脚乱地修。5.5 常见问题速查表现象可能原因排查方法应对措施事件到了但没有执行动作事件类型不匹配、条件表达式返回False、规则被禁用检查命中规则计数、逐个规则调试调整accept配置、修正条件表达式动作重复执行调用方重试、事件去重窗口过期查询Redis去重记录、看执行记录数量延长去重窗口、完善幂等键逻辑任务延迟严重Worker过少、动作超时过长、队列积压查看队列深度、动作耗时分布调整Worker数量、缩短超时、改异步内存持续增长缓存无上限、队列无限积压抓内存快照、检查全局对象添加缓存LRU、设置队列上限重启后任务丢失执行状态没有持久化查数据库执行记录任务状态落库、启动时恢复pending任务6. 后续扩展方向与个人经验6.1 规则可视化配置与管理端rea目前的使用方式是通过YAML文件配置规则改配置需要重新加载或重启进程。对小规模场景够用但业务方如果经常需要调整规则体验就不够好。后续扩展方向上第一个想到的是给规则加一个管理端页面。Engine暴露出配置接口管理端调用接口增删改规则改动即时生效不重启进程。这里涉及动态加载规则和规则版本管理两个问题都是可以单独展开的话题。我们内部已经验证了可行性把规则的存取从文件切换到了数据库引擎定期刷新配置下发延迟控制在秒级。再往后可以做规则调试模式。每条事件执行的时候把规则解析和匹配的中间结果都记下来方便业务方在页面上查看“为什么这条规则没命中”。这在排障时会省下大量沟通成本。6.2 动作编插件化目前动作类型就HttpAction、ShellAction、NotifyAction三种都是写死在代码里的。扩展新动作类型需要改代码升级不符合最初的“新增规则不改代码”理念。下一步打算做一个简单的插件机制定义一个Action接口任何只要实现execute方法并能返回ActionResult的模块通过配置文件注册路径后就能被引擎加载。这样平台里其他团队也可以贡献自己的动作插件不需要合到rea主仓库里。插件机制实现不难但沙箱和安全策略要先想好因为插件是执行任意代码的入口不能让人随便传一个恶意的路径进去执行。我们的设想是插件只在启动时注册配置来源受管控运行时不做动态加载。6.3 个人经验几个让我印象深刻的教训踩过这些坑之后最大的体会是规则引擎这类中间件难点往往不在引擎本身而在围绕引擎的配套可观测性、幂等设计、状态管理、限制机制。写一个能跑通的引擎用不了多少时间难的是让它能长期稳定地跑在别人看不到的角落里不出问题。另一个经验是规则配置一定要有人审查。rea配置本身很灵活可以访问payload里的任意字段可以调用任意外部动作。如果业务方对payload结构理解不到位很容易写出永远命中不了的条件或者刚好命中太多条的过于宽泛的条件。我们在上线早期加了一个“规则评审”流程每条规则要经过引擎负责人的确认才能上线这帮助避开了不少低级事故。最后再分享一个小技巧调试规则的时候不要依赖线上日志空想本地起一个rea实例构造一条假的Event用调试模式跑一遍直接看规则解析结果和命中结果。比起改线上配置试错这种方式既快又安全。哪怕现在管理工具已经很完善了我这个本地调试的习惯也没丢。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑