今天从一个实际案例入手,介绍站在架构师的角度,如何识别并定义问题,提炼需求,技术方案选型,再到详细设计,最后利用AI的能力协助写出核心的代码,验证与调优。
解决问题存在一定的模式,也可以称之为框架,总结出自己的思考和解题框架,以后再碰到同类型的问题就可以如庖丁解牛一样容易。
很多年前,我写代码仍然是if else,最多加个for,也就是所谓的面条式编程。
这三板斧应付简单的业务也没有太大问题,大不了50行代码能搞定的事,我就写个500行。但是一旦需要写一些稍微偏底层的框架,就无从下手。
不过现在的研发工程师比我们以前幸运很多,借助于AI辅助编码,只要把设计写出来,由AI协助实现一个稍微复杂一点的框架也不是什么大问题。
2. 识别问题
在支付系统中,有一个基本的需求就是流程编排,对应的模块就是流程引擎。
比如收银支付需要把整个支付流程串起来,包括用户校验,商户校验,合约校验,风控校验,余额校验等;商户入驻需要针对不同类型的商户串不同的流程;外发渠道请求支付时需要针对不同的渠道交互编排不同的流程。
以渠道流程编排为例,我刚入行第三方支付时,还没有断直连,快捷支付对接了中工农建交招等各种银行,当然还有银联。不同的渠道交互模式是不一样的,有些是先提供独立的签约接口,有些是签约+支付合并在一个接口,有些需要由渠道发送并验证短信验证码,有些需要由我方发送并验证短信验证码,有些需要先刷新token后,再去做支付。
下面是一个典型的渠道交互。
当时没有经验,于是每个渠道写一套独立的接入代码,各种逻辑都能通过if else编排出来。
但实际上,这些交互都可以分解成一个个组件操作,然后通过流程引擎做一个编排,就可以减少很多重复的代码,也能提高系统的健壮性,还能减少经验不足的工程师出现问题的可能性。
小结一下,识别问题:为每个渠道写独立的代码,冗余代码过多,可维护性差,研发效率低,需要有一个流程编排的能力。
3. 明确需求
针对不同的渠道接入模式,配置出一些通用的配置,然后通过流程引擎驱动流程配置,完成和渠道的交互,减少冗余代码,提高可维护性。
4. 方案选型
有个经典的“奥卡姆剃刀原理”,核心思想是:“如无必要,勿增实体”。所以我们先看看市场上有哪些解决方案。
流程引擎在企业软件中无处不在,尤其是内部审批业务里面。市场上的确有很多现成的流程引擎,老牌的比如jBPM,Activiti等都很知名,中国后起之秀且开源的项目liteflow也是其中佼佼者。
其中jBPM,Activiti的配置过于繁琐和复杂,一个简单的支付流程,可能有上百行的配置,放弃。
liteflow的配置非常简单,可以做为备选方案。liteflow的核心思想:所有的操作都是组件,把组件串起来,完成业务能力。
而liteflow的问题之一也是因为其配置过于简单,把业务推进以及判断逻辑全部放在代码中去,导致在配置时无法看到推进的条件。比如下面是一个支付流程:
<flow>
<chain name="commonPayChain">
THEN(transitionToProcess, requestPay,
SWITH(subOrderStateSwitch).to(transitionToSuccess, transitionToFail))
</chain>
</flow>
我们只知道先推进到支付中,再发起请求,然后判断子订单状态,最后推进到成功或失败。
如果我们一定要选择业界现有的技术解决方案,个人推荐使用liteflow。
还有一个方案,就是自己实现一个订制化的流程引擎。制定流程引擎有几个诉求:
配置要简单清晰。
各种触发条件、推进条件、操作等需要在配置文件中体现。
配置文件也有多种格式,比如XML,YAML,JSON等。除了这几种,还有另外一种选择:自定义一种语法,使用链式处理。
市面上也很多这样使用的。
在测试Mock框架Mockito中,我们经常这样使用:
when(mockedList.get(0)).thenReturn("first element");
在java的流式代码中,我们经常这样使用:
names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
在jQuery中,我们经常这样使用(很多年前我也写JQuery):
$('#element').css('color', 'red').show()
.click(function() { alert('Clicked!'); });
所有以上这些都有一个共同的特点:链式调用。
链式调用有个好处:非常接近自然语言,容易理解。最终选择此方案。
5. 详细设计
使用链式调用的形式编写配置文件,易于理解和配置。关键字和示例在后面有。
5.1. 整体架构图
说明:
流程引擎在启动时加载所有配置,生成不同的处理链。
根据请求的参数,找到对应的链。
根据处理链的配置,从第一个节点开始,驱动调用不同的组件,直接全部节点运行完毕。
5.2. 状态、事件、操作
分解支付业务的处理到最小粒度,只有三个:订单状态,当前的事件,触发的操作。
也就是:在指定主订单状态的情况下,当前过来了什么事件,进而触发什么样的操作。
状态(State):表示流程中的不同阶段,例如:
INIT
、PROCESS
、SUCCESS
、FAIL
。事件(Event):触发状态转移的事件,例如:
CREATE
、QUERY
、CALLBACK
。操作(Operation):需要执行的操作,例如:
PAY
、PAY_QUERY
、REFUND
。
5.3. 定义关键字
和前面Mockitor或JAVA流式处理一样,链式调用需要有一些专用语法。对于支付流程来说,有以下几个关键字:
whenOrderState:初始条件,主订单状态要满足给定条件。比如:whenOrderState(CommonOrderState.INIT)。
onEvent:触发事件。比如:onEvent(CommonEvent.CREATE)。
transitionOrderStateTo:推进主订单状态。比如:transitionOrderStateTo(CommonOrderState.PROCESS)。
request:请求操作。比如:request(CommonOperation.PAY)。
when:判断request返回的数据,比如:when("subOrder.currentState == SubOrderState.S")。
notify:发出订单消息给其它域。比如:notify()。
then:进入子流程。比如:when("subOrder.currentState == SubOrderState.S")
.then(xxx)。subFlow:子流程标识。比如subFlow().request(xxx)。
下面是一个通用支付配置的示例:
whenOrderState(CommonOrderState.INIT)
.onEvent(CommonEvent.CREATE)
.transitionOrderStateTo(CommonOrderState.PROCESS)
.request(CommonOperation.PAY)
.when("subOrder.currentState == SubOrderState.S")
.transitionOrderStateTo(CommonOrderState.SUCCESS)
.when("subOrder.currentState == SubOrderState.F")
.transitionOrderStateTo(CommonOrderState.FAIL)
.when("subOrder.currentState == SubOrderState.U && subOrder.webForm != null")
.notifyNode();
简单说明一下:
当初始状态为INIT(初始),触发了CREATE(创建),主订单状态流转到PROCESS(处理中),并发起请求PAY(支付)。
支付结果回来后,当子订单结果为S(成功)就推进主订单成功,当子订单结果为F(失败)就推进主订单失败,当子订单结果为U(未知)且webForm(表单)不为空就发出消息通知其它域。
5.4. 支持Groovy脚本
需要支持配置中的groovy脚本。比如.when("subOrder.currentState == SubOrderState.F")中"subOrder.currentState == SubOrderState.F"就是groovy脚本,当脚本结果为真,就执行后面的操作。
注意在groovy脚本引擎上下文中导入必要的类,包括但不限于 SubOrderState等类,否则脚本运行会报错。
5.5. 核心组件
主要有以下几大类组件:
枚举:状态、事件等枚举。
模型:主订单、子订单等模型。
配置:支付、退款、支付查询等配置。
上下文:链式处理的上下文。
处理器:推进状态,外发渠道,解析回调等处理器。
详细如下:
com.yinmo.flowengine
|-enums
: 枚举,包括状态,事件等
| |-FlowOperation
: 操作枚举基类
| |-CommonOperation
: 通用操作类型:支付,支付查询,支付回调等
| |-FlowState
: 状态基类
| |-CommonOrderState
: 通用订单状态
| |-SubOrderState
: 子订单状态
| |-FlowEvent
: 事件基类
| |-CommonEvent
: 通用事件类型
|-handler
: 处理器
| |-HandlerNodeType
: 节点处理器类型
| |-HandlerNode
: 节点处理器
| |-SubOrderHandler
: 子订单处理器
| |-SubOrderSendHandler
: 子订单发送处理器
| |-SubOrderCallbackHandler
: 子订单回调处理器
|-config
:配置
| |-FlowConfig
: 流程配置接口
| |-AbstractConfig
: 抽象流程配置
| |-CommonPayConfig
: 通用支付配置类
| |-RefreshTokenPayConfig
: 刷新TOKEN支付配置类
| |-CommonPayQueryConfig
: 通用支付查询配置类
| |-CommonPayCallbackConfig
: 通用支付回调配置类
|-context
:上下文
| |-FlowContext
: 流程上下文
| |-GatewayCallbackContext
: 网关回调上下文
|-com.yinmo.flowengine.model
:模型
| |-Order
: 主订单
| |-SubOrder
: 子订单
|-com.yinmo.flowengine.service
:服务
| |-FlowEngineService
: 流程引擎服务接口
| |-impl.FlowEngineServiceImpl
: 流程引擎服务实现
|-com.yinmo.flowengine.test
:测试类
|-CommonPayTest
: 通用支付测试
|-RefreshTokenPayTest
: 刷新TOKEN支付测试
|-CommonPayQueryTest
: 通用支付查询测试
|-CommonPayCallbackTest
: 通用支付回调测试
需要特别说明的几点:
状态、事件等枚举都先定义接口做为基类,主要是考虑扩展性。
配置根据渠道模式来配置,比如有50条渠道,抽象出10个模式,只需要配置10个配置文件就行。
5.6. 部分核心类定义说明
这些描述主要用于生成ChatGPT的提示词用的,ChatGPT能理解并可以在生成代码时生成对应的注释。
模型:
Order
模型:属性包括:String orderId;FlowState previousState;FlowState currentState;Money transactionAmount;List subOrders;String webForm等。
增加方法:transitionToState(FlowState)。
SubOrder
模型:属性包含:String subOrderId, String parentOrderId,SubOrderState previousState, SubOrderState currentState, Money transactionAmount, String channelResponseCode, String channelResponseMessage, String standardResponseCode, String standardResponseMessage, String webForm, String sendToChannelContext, String receiveFromChannelContext等。
增加方法:transitionToState(SubOrderState)。
枚举:
定义interface:FlowState,FlowEvent,FlowOperation。所有枚举全部继承于interface,方便扩展。
interface FlowOperation增加方法:
String name()
、String getMethod()
和boolean isCallback()
。isCallback()用于表示是否是一个外部渠道回调操作,回调是直接解析,不需要发给渠道。getMethod()获取方法名,会发给网关,网关根据这个参数组装发给外部渠道的参数。通用主订单状态:enum CommonOrderState implements FlowState。
有:INIT, PROCESS, SUCCESS, FAIL。
通用子订单状态:enum SubOrderState implements FlowState。
有:I(Init), U(Unkown), S(Success), F(Fail)。
通用操作类型:enum CommonOperation implements FlowOperation。
有:PAY("pay", false), PAY_QUERY("payQuery", false), REFUND("refund", false), REFRESH_TOKEN("refreshToken", false), PARSE("parse", true)等。
示例:PAY("pay", false)为例:说明是一个支付操作,“pay"会发给网关,网关根据这个参数组装发给外部渠道的参数,“false"表示是否是一个回调。
通用事件:enum CommonEvent implements FlowEvent。
有:CREATE(订单创建事件,驱动系统向渠道发起支付退款等操作), QUERY(订单查询事件,驱动系统向渠道发起查询操作), CALLBACK(外部渠道主动回调通知事件,驱动系统直接解析网关接收到的报文)。
上下文:
FlowContext 。
属性:
channelName
、order
、event
、configName
、gatewayCallbackContext
和subOrder
等。GatewayCallbackContext。
属性:
subOrderId
、state
、callbackMessage
、channelResponseCode
、channelResponseMessage
、standardResponseCode
、standardResponseMessage
等。上下文用于在处理器和服务之间传递需要处理的数据。
配置相关:
interface FlowConfig。
方法:String name(),HandlerNode getHandlerNode(FlowState orderState, FlowEvent event)。
abstract class
AbstractConfig
implements FlowConfig。核心能力:使用构建器模式来定义流程。
属性:
Map<FlowState, Map<FlowEvent, HandlerNode>>
存储配置。方法:void init();abstract initConfig()让子类实现。在@PostConstruct init()调用initConfig进行初始化,子类在initConfig()配置脚本。
提供方法定义状态、事件、转换、请求和通知。
所有具体的流程配置类继续自AbstractConfig。所有流程配置类提供String name()方法,返回当前的类简要名称用于标识流程名称。示例:getClass().getSimpleName()。
定义子类:
CommonPayConfig
,RefreshTokenPayConfig
,CommonPayQueryConfig
,CommonPayCallbackConfig
,全部继承自AbstractConfig
。
处理节点:
节点处理器类型:enum HandlerNodeType。
有:TRANSITION(推进主订单状态变更),REQUEST(向外部渠道发送请求,或解析外部渠道回调报文),NOTIFY(发送消息通知其他域)
处理器节点:class
HandlerNode
,表示流程中的一个节点。属性:节点类型、下一个状态、操作、回调处理器和子节点列表等字段。
确保
HandlerNode
能通过其子节点来完成链式操作。
处理器和服务:
class
SubOrderHandler
。方法:void handle(FlowContext, CommonOperation)。
根据操作委托给
SubOrderSendHandler
或SubOrderCallbackHandler
。class SubOrderSendHandler
。方法:void handle(FlowContext, CommonOperation)。
创建子订单,保存到数据库,组装网关报文,发送给网关,由网关发送给外部渠道,解析渠道返回的报文,更新子订单,保存子订单最新数据到数据库。把子订单放到FlowContext中。
class SubOrderCallbackHandler。
方法:void handle(FlowContext, CommonOperation)。
创建子订单,解析渠道主动回调通知返回的报文,更新子订单,保存子订单最新数据到数据库。把子订单放到FlowContext中。
interface
FlowEngineService
。void execute(FlowContext);
class FlowEngineServiceImpl implements
FlowEngineServiceImpl
。功能:执行从配置生成的处理节点列表来处理流程。迭代处理节点,并根据需要处理链式操作,通过递归处理子节点来实现。
属性:@Autowired List configs, Map<String, Config> configMap;
方法:@PostConstruct init()方法用于初始化。在init()方法中,把自动装配的List configs转存到configMap,在execute方法中直接使用configMap.get(context.getConfigName())获取配置。
其它:初始化完成后,使用fastjson格式化打印configMap的内容。
日志:
在需要日志的类中使用
@Slf4j
。以下情况需要打印日志:关键处理,状态变更,只写了空方法还没有具体实现的方法,异常。
测试:
编写测试类:
CommonPayTest
,CommonPayQueyTest
,CommonPayCallbackConfig
,RefreshTokenPayTest。所有测试类使用Mockitor进行mock子订单的返回值。
数据校验使用断言。
所有测试类需要有三个方法:testSubOrderSuccess(), testSubOrderSuccessFail(),testSubOrderUnkown()。分别是子订单返回成功,返回失败,返回未知。
6. 代码实现
代码全部由ChatGPT生成。为减少篇幅,这里只展示了核心的两个类。
也可以根据提示词自己生成,生成代码的完整提示词,在前面的文章有详细介绍:https://mp.weixin.qq.com/s/XtMUIHBuJs7trnsbdgHjMg。
抽象配置类AbstractConfig,提供whenOrderState, onEvent, when,then,subFlow,request, transitionOrderStateTo等自定义关键字的实现逻辑。
abstract class AbstractConfig implements FlowConfig {
protected Map<FlowState, Map<FlowEvent, HandlerNode>> configMap = new HashMap<>();
@PostConstruct
public void init() {
initConfig();
}
/**
* 子类实现该方法来初始化配置
*/
protected abstract void initConfig();
@Override
public HandlerNode getHandlerNode(FlowState orderState, FlowEvent event) {
Map<FlowEvent, HandlerNode> eventMap = configMap.get(orderState);
if (eventMap != null) {
return eventMap.get(event);
}
return null;
}
protected NodeBuilder whenOrderState(FlowState orderState) {
Map<FlowEvent, HandlerNode> eventMap = configMap.computeIfAbsent(orderState, k -> new HashMap<>());
return new NodeBuilder(eventMap, null);
}
protected NodeBuilder subFlow() {
HandlerNode subFlowNode = new HandlerNode(null);
subFlowNode.setSubFlow(true); // 标记为子流程节点
return new NodeBuilder(null, subFlowNode);
}
protected class NodeBuilder {
private Map<FlowEvent, HandlerNode> eventMap;
private HandlerNode rootNode;
private HandlerNode currentNode;
public NodeBuilder(Map<FlowEvent, HandlerNode> eventMap, HandlerNode rootNode) {
this.eventMap = eventMap;
this.rootNode = rootNode;
this.currentNode = rootNode;
}
public NodeBuilder onEvent(FlowEvent event) {
if (eventMap != null) {
HandlerNode eventNode = new HandlerNode(null);
eventMap.put(event, eventNode);
this.rootNode = eventNode;
this.currentNode = eventNode;
} else {
throw new IllegalStateException("子流程中不能调用 onEvent()");
}
return this;
}
public NodeBuilder transitionOrderStateTo(FlowState nextState) {
currentNode.setNodeType(HandlerNodeType.TRANSITION);
currentNode.setNextState(nextState);
return this;
}
public NodeBuilder request(FlowOperation operation) {
HandlerNode requestNode = new HandlerNode(HandlerNodeType.REQUEST);
requestNode.setOperation(operation);
currentNode.addChild(requestNode);
this.rootNode = requestNode;
this.currentNode = requestNode;
return this;
}
public NodeBuilder when(String condition) {
HandlerNode whenNode = new HandlerNode(null);
whenNode.setConditionScript(condition);
rootNode.addChild(whenNode);
this.currentNode = whenNode;
return this;
}
public NodeBuilder then(NodeBuilder nodeBuilder) {
currentNode.addChild(nodeBuilder.rootNode);
return this;
}
public NodeBuilder notifyNode() {
HandlerNode notifyNode = new HandlerNode(HandlerNodeType.NOTIFY);
currentNode.addChild(notifyNode);
return this;
}
public HandlerNode getCurrentNode() {
return currentNode;
}
}
}
流程引擎入口实现类:FlowEngineServiceImpl,提供链式处理,并调用各处理器完成业务处理。
public class FlowEngineServiceImpl implements FlowEngineService {
private final Map<String, FlowConfig> configMap = new HashMap<>();
private List<FlowConfig> configs;
private SubOrderHandler subOrderHandler;
public void init() {
for (FlowConfig config : configs) {
configMap.put(config.name(), config);
}
log.info("ConfigMap initialized with configs: {}", JSON.toJSONString(configMap));
}
public void execute(FlowContext context) {
FlowConfig config = configMap.get(context.getConfigName());
if (config == null) {
log.error("No configuration found for name: {}", context.getConfigName());
return;
}
FlowState currentState = context.getOrder().getCurrentState();
FlowEvent event = context.getEvent();
log.info("当前主订单状态:{}, 触发事件:{}", currentState.name(), event.name());
HandlerNode handlerNode = config.getHandlerNode(currentState, event);
if (handlerNode == null) {
log.error("No handler node found for state: {} and event: {}", currentState, event);
return;
}
executeNode(context, handlerNode);
}
private void executeNode(FlowContext context, HandlerNode node) {
// 处理节点,根据节点类型执行相应的操作
if (node.getNodeType() != null) {
switch (node.getNodeType()) {
case TRANSITION:
// 推进主订单状态变更
context.getOrder().transitionToState(node.getNextState());
log.info("主订单状态变更为:{} -> {}", context.getOrder().getPreviousState(), context.getOrder().getCurrentState());
break;
case REQUEST:
// 向外部渠道发送请求,或解析外部渠道回调报文
subOrderHandler.handle(context, (CommonOperation) node.getOperation());
log.info("执行操作:{}", node.getOperation());
break;
case NOTIFY:
// 发送消息通知其他域
notifyOtherDomains(context);
log.info("通知其它域:{}", context.getOrder());
break;
default:
log.warn("未知的节点类型: {}", node.getNodeType());
}
}
// 处理子节点
if (node.getChildren() != null) {
for (HandlerNode child : node.getChildren()) {
// 如果有条件,执行Groovy脚本判断
boolean conditionMet = true;
if (child.getConditionScript() != null) {
conditionMet = evaluateCondition(context, child.getConditionScript());
}
if (conditionMet) {
executeNode(context, child);
// 如果条件满足,执行后不再检查其他兄弟节点
break;
}
}
}
}
private boolean evaluateCondition(FlowContext context, String conditionScript) {
// 使用Groovy脚本引擎执行条件
try {
GroovyScriptEngineImpl scriptEngine = new GroovyScriptEngineImpl();
// 设置脚本上下文
scriptEngine.put("subOrder", context.getSubOrder());
scriptEngine.put("order", context.getOrder());
// 将必要的导入语句和条件脚本拼接在一起
String fullScript = "import com.yinmo.flowengine.enums.*;\n" + conditionScript;
Object result = scriptEngine.eval(fullScript);
log.info("执行判断:{}, 结果:{}", conditionScript, result);
if (result instanceof Boolean) {
return (Boolean) result;
} else {
log.warn("条件脚本未返回布尔值: {}", conditionScript);
return false;
}
} catch (Exception e) {
log.error("执行条件脚本时出错: {}", conditionScript, e);
return false;
}
}
private void notifyOtherDomains(FlowContext context) {
// TODO: 实现通知其他域的逻辑
log.info("Notify other domains with context: {}", context);
}
}
7. 验证与调优
多写几份配置文件,比如支付,退款,支付查询,刷新后支付,回调通知等,多跑几个测试用例,正常,异常等,测试类要有基本的断言。如果有报错或不符合预期,就扔给ChatGPT去修复。
支付成功测试:
支付失败测试:
其中的一小段测试代码:
public void testSubOrderSuccess() {
// 设置Mock行为
SubOrder subOrder = new SubOrder();
subOrder.setCurrentState(SubOrderState.S);
Mockito.doAnswer(invocation -> {
FlowContext context = invocation.getArgument(0);
context.setSubOrder(subOrder);
return null;
}).when(subOrderSendHandler).handle(Mockito.any(), Mockito.any());
// 构建上下文
Order order = new Order();
order.setOrderId("ORDER123");
order.setCurrentState(CommonOrderState.INIT);
FlowContext context = new FlowContext();
context.setOrder(order);
context.setEvent(CommonEvent.CREATE);
context.setConfigName("CommonPayConfig");
// 执行流程
flowEngineService.execute(context);
// 验证结果
Assert.assertEquals(CommonOrderState.SUCCESS, order.getCurrentState());
}
8. 结束语
解决问题是我们研发工程师们的核心竞争力。
本文以“为支付流程这个细分领域构建一套专用的极轻量级流程引擎”为例子,介绍了在工作中如何做问题抽象,参考行业经验,设计解决方案,并利用AI辅助实现几乎所有的代码。希望能为大家在提高解决工作问题的能力方面提供一些有益的参考。
这里面有一个重要前提是做好知识储备,否则仍然无法顺利解决碰到的工作问题。拿这个案例来说,我们需要了解流程编排的特性,知道流程编排适用于哪些场景,行业有哪些流程引擎,各自的特点是什么,了解链式处理的特点,也需要了解如何使用AI辅助写复杂逻辑代码等。
具体使用ChatGPT生成全套流程引擎的代码,请参考上一篇文章:https://mp.weixin.qq.com/s/XtMUIHBuJs7trnsbdgHjMg。如果需要GPT生成的完整代码,请私信。
这是《图解支付系统设计与实现》专栏系列文章中的番外第(3)篇。欢迎关注公众号“隐墨星辰”,和我一起深入解码支付系统设计与实现的方方面面。