• camunda7流程跳转和流程退回的实现方法


    我们在使用工作流的时候,常常有“流程退回”、“流程跳转”、“自由流”、“动态加签”等这样的需求。Camunda流程平台提供了这样的机制和接口,虽然流程模型定义活动执行顺序的序列流,但有时需要灵活地重新启动活动或取消正在运行的活动进而可以实现中国特色的流程需求。文本重点讲如何使用camunda的API接口实现流程跳转、流程退回的需求,另外还可能适用的场景有:

    • 实现中国特色流程操作,包括:退回申请人、退回上一步、任意退回、流程跳转、流程撤销、动态增加活动等;
    • 修复必须重复或跳过某些步骤的流程实例
    • 将流程实例从一个版本的流程定义迁移到另一个版本
    • 测试:可以跳过或重复活动,以对单个工艺段进行孤立测试

    比如:云程低代码平台,可以通过界面配置实现流程操作的特殊需求:

    在线体验系统:http://www.yunchengxc.com

    为了执行这样的操作,流程引擎提供了通过RuntimeService.createProcessInstanceModification(…)或RuntimeService.createModification…输入的流程实例修改API。这个API允许通过使用fluent生成器在一个调用中指定多个修改指令。特别是,可以:

    • activity活动之前开始执行
    • 在离开activity活动的序列流上开始执行
    • 取消正在运行的 Activity 实例
    • 取消给定activity活动的所有正在运行的实例
    • 使用每个指令设置变量

    1、通过一个示例介绍如何动态修改流程实例

    例如,请考虑以下BPMN流程模型:

    该模型显示了处理贷款申请的简单流程。让我们假设一个贷款申请已经到达,贷款申请已经过评估,并且决定拒绝该申请。这意味着流程实例具有以下活动实例状态:

    1. ProcessInstance
    2. Decline Loan Application

    现在,执行任务“拒绝贷款申请”的工作人员认识到评估结果中的错误,并得出结论,尽管如此,该申请仍应被接受。虽然这种灵活性不是作为流程的一部分建模的,但流程实例修改允许更正正在运行的流程实例。下面的API调用实现了这个技巧::

    1. ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .startBeforeActivity("acceptLoanApplication")
    4. .cancelAllForActivity("declineLoanApplication")
    5. .execute();

    此命令首先在活动Accept Loan Application之前开始执行,直到达到等待状态(在本例中为用户任务的创建)。之后,它将取消活动“拒绝贷款申请”的运行实例。在工作人员的任务列表中,“拒绝”任务已被删除,并显示“接受”任务。生成的活动实例状态为:

    1. ProcessInstance
    2. Accept Loan Application

    假设在批准应用程序时必须存在一个名为approver的变量。这可以通过如下扩展修改请求来实现:

    1. ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .startBeforeActivity("acceptLoanApplication")
    4. .setVariable("approver", "joe")
    5. .cancelAllForActivity("declineLoanApplication")
    6. .execute();

    添加的setVariable调用确保在启动活动之前提交指定的变量。

    现在来看一些更复杂的案例。假设申请再次不正常,并且“拒绝贷款申请”活动处于活动状态。现在,工人意识到评估流程是错误的,并希望完全重新启动。以下修改说明表示执行此任务的修改请求:

    可以启动子流程活动:

    1. ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .cancelAllForActivity("declineLoanApplication")
    4. .startBeforeActivity("assessCreditWorthiness")
    5. .startBeforeActivity("registerApplication")
    6. .execute();

    要从子流程的 start 事件开始,请执行以下操作:

    1. ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .cancelAllForActivity("declineLoanApplication")
    4. .startBeforeActivity("subProcessStartEvent")
    5. .execute();

    要启动子流程本身,请执行以下操作:

    1. ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .cancelAllForActivity("declineLoanApplication")
    4. .startBeforeActivity("evaluateLoanApplication")
    5. .execute();

    要启动流程的 start 事件,请执行以下操作:

    1. ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().singleResult();
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .cancelAllForActivity("declineLoanApplication")
    4. .startBeforeActivity("processStartEvent")
    5. .execute();

    1.1、流程跳转的实现方法

    流程实例执行流程中,经常遇到特殊业务需求,需要跳过某些流程活动节点。为此,您可以启动一个带有修改的流程实例,并将令牌直接放在该流程实例中。

    假设您想跳过子流程 Evaluate Loan Application 并测试网关 Application OK? 使用您的流程变量,您可以使用以下命令启动流程实例

    1. ProcessInstance processInstance = runtimeService.createProcessInstanceByKey("Loan_Application")
    2. .startBeforeActivity("application_OK")
    3. .setVariable("approved", true)
    4. .execute();

    此方法也可以用在JUnit测试中可以跳过前面流程部分,仅仅关注要测试的流程活动。

    2、操作语义

    以下各节指定了流程实例修改的确切语义,应阅读这些语义,以便了解不同情况下的修改效果。如果没有另行说明,以下示例将参考以下流程模型进行说明:

    2.1、修改指令类型

    Fluent 流程实例修改生成器提供以下方法接口

    • startBeforeActivity(String activityId)
    • startBeforeActivity(String activityId, String ancestorActivityInstanceId)
    • startAfterActivity(String activityId)
    • startAfterActivity(String activityId, String ancestorActivityInstanceId)
    • startTransition(String transitionId)
    • startTransition(String transition, String ancestorActivityInstanceId)
    • cancelActivityInstance(String activityInstanceId)
    • cancelTransitionInstance(String transitionInstanceId)
    • cancelAllForActivity(String activityId)

    2.1.1、在活动开始之前开始

    1. ProcessInstanceModificationBuilder#startBeforeActivity(String activityId)
    2. ProcessInstanceModificationBuilder#startBeforeActivity(String activityId, String ancestorActivityInstanceId)

    通过startBeforeActivity在活动之前启动意味着在进入活动之前就开始执行。该指令尊重asyncBefore标志,这意味着如果活动是asyncBeBefore,则将创建作业。通常,此指令从指定的活动开始执行流程模型,直到达到等待状态。有关等待状态的详细信息,请参阅“流程中的事务”文档。

    2.1.2、活动后开始

    1. ProcessInstanceModificationBuilder#startAfterActivity(String activityId)
    2. ProcessInstanceModificationBuilder#startAfterActivity(String activityId, String ancestorActivityInstanceId)

    通过startAfterActivity在活动之后启动意味着在活动的单个传出序列流上开始执行。该指令不考虑给定活动的asyncAfter标志。如果有多个传出序列流,或者根本没有,则指令失败。如果成功,此指令将从序列流开始执行流程模型,直到达到等待状态。

    2.1.3、开始流转

    1. ProcessInstanceModificationBuilder#startTransition(String transitionId)
    2. ProcessInstanceModificationBuilder#startTransition(String transition, String ancestorActivityInstanceId)

    通过startTransition启动转换转换为在给定的序列流上开始执行。当存在多个传出序列流时,这可以与startAfterActivity一起使用。如果成功,此指令将从序列流开始执行流程模型,直到达到等待状态。

    2.1.4、取消活动实例

    ProcessInstanceModificationBuilder#cancelActivityInstance(String activityInstanceId)

    cancelActivityInstance可以取消特定的活动实例。这可以是叶活动实例,例如用户任务的实例,也可以是层次结构中更高范围的实例,例如子流程的实例。请参阅有关活动实例的详细信息—如何检索流程实例的活动实例。

    2.1.5、取消转换实例

    ProcessInstanceModificationBuilder#cancelTransitionInstance(String activityInstanceId)

    转换实例表示即将以异步延续的形式进入/离开活动的执行流。已创建但尚未执行的异步延续作业表示为转换实例。cancelTransitionInstance可以取消这些实例。请参阅有关活动和转换实例的详细信息—如何检索流程实例的转换实例。

    2.1.6、取消活动的所有活动实例

    ProcessInstanceModificationBuilder#cancelAllForActivity(String activityId)

    为了方便起见,还可以通过指令cancelAllForActivity取消给定活动的所有活动和转换实例。

    2.2、提供或设置变量

    对于每个实例化指令(即startBeforeActivity、startAfterActivity或startTransition),都可以提交流程变量。API提供了方法

    • setVariable(String name, Object value)
    • setVariables(Map variables)
    • setVariableLocal(String name, Object value)
    • setVariablesLocal(Map variables)

    变量是在创建实例化所需的作用域之后、指定元素的实际执行开始之前设置的。这意味着,在流程引擎历史记录中,这些变量看起来不像是在执行startBefore和startAfter指令的指定活动期间设置的。在即将执行指令(即进入活动等)的执行上设置局部变量。

    2.3、基于活动实例的 API

    流程实例修改API基于活动实例。流程实例的活动实例树可以通过以下方法进行检索:

    1. ProcessInstance processInstance = ...;
    2. ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId());

    ActivityInstance是一个递归数据结构,上面方法调用返回的活动实例表示流程实例。ActivityInstance对象的ID可用于取消特定实例或用于实例化期间的祖先选择。

    接口ActivityInstance具有方法getChildActivityInstances和getChildTransitionInstances以在活动实例树中进行深入搜索。例如,假设活动“评估信用价值”和“注册申请”处于活动状态。然后活动实例树如下所示:

    1. ProcessInstance
    2. Evaluate Loan Application
    3. Assess Credit Worthiness
    4. Register Application Request

    在代码中,可以按如下方式检索 Assess 和 Register 活动实例:

    1. ProcessInstance processInstance = ...;
    2. ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId());
    3. ActivityInstance subProcessInstance = activityInstance.getChildActivityInstances()[0];
    4. ActivityInstance[] leafActivityInstances = subProcessInstance.getChildActivityInstances();// leafActivityInstances has two elements; one for each activity

    还可以直接检索给定活动的所有活动实例:

    1. ProcessInstance processInstance = ...;
    2. ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId());
    3. ActivityInstance assessCreditWorthinessInstances = activityInstance.getActivityInstances("assessCreditWorthiness")[0];

    与活动实例相比,转换实例并不表示活动的活动,而是表示即将进入或即将离开的活动。当存在异步延续的作业但尚未执行时,就会出现这种情况。对于活动实例,可以使用getChildTransitionInstances方法检索子转换实例,转换实例的API与活动实例的类似。

    2.4、嵌套实例化

    假设上面示例流程的一个流程实例,其中活动“拒绝贷款申请”处于活动状态。现在,我们提交指示,以便在活动评估信用价值之前开始。应用此指令时,流程引擎确保实例化所有尚未激活的父作用域。在这种情况下,在启动活动之前,流程引擎会实例化Evaluate Loan Application子流程。在活动实例树之前的位置

    1. ProcessInstance
    2. Decline Loan Application

    现在是

    1. ProcessInstance
    2. Decline Loan Application
    3. Evaluate Loan Application
    4. Assess Credit Worthiness

    除了实例化这些父作用域,引擎还确保在这些作用域中注册事件订阅和作业。例如,考虑以下流程

    启动活动“评估信用价值”还会注册消息边界事件“收到的取消通知”的事件订阅,以便可以通过这种方式取消子流程。

    2.5、实例化的祖先选择

    默认情况下,启动活动会实例化所有尚未实例化的父作用域。当活动实例树如下时:

    1. ProcessInstance
    2. Decline Loan Application

    然后,启动“评估信用价值”(Assess Credit Worthiness)会在以下更新的树中生成:

    1. ProcessInstance
    2. Decline Loan Application
    3. Evaluate Loan Application
    4. Assess Credit Worthiness

    流程范围也已实例化。现在假设子流程已经实例化,如下图所示:

    1. ProcessInstance
    2. Evaluate Loan Application
    3. Assess Credit Worthiness

    再次启动 Assess Credit Worthiness 将在现有子流程实例的上下文中启动它,因此生成的树为:

    1. ProcessInstance
    2. Evaluate Loan Application
    3. Assess Credit Worthiness
    4. Assess Credit Worthiness

    如果您想避免这种行为,而是想第二次实例化子流程,则可以使用方法startBeforeActivity(String activityId,String ancestorActivityInstanceId)来提供祖先活动实例的id-类似的方法用于在活动之后启动和启动转换。参数ancestorActivityInstanceId采用当前活动的活动实例的id,该活动实例属于要启动的活动的祖先活动。如果一个活动包含要启动的活动(直接或间接与其间的其他活动一起),那么它就是一个有效的祖先。

    对于给定的祖先活动实例id,祖先活动和要启动的活动之间的所有作用域都将被实例化,而不管它们是否已经被实例化。在该示例中,以下代码以流程实例(作为根活动实例)作为祖先启动活动“评估信用价值”:

    1. ProcessInstance processInstance = ...;
    2. ActivityInstance activityInstanceTree = runtimeService.getActivityInstance(processInstance.getId());
    3. runtimeService.createProcessInstanceModification(activityInstanceTree.getId())
    4. .startBeforeActivity("assessCreditWorthiness", processInstance.getId())
    5. .execute();

    然后,生成的活动实例树如下所示:

    1. ProcessInstance
    2. Evaluate Loan Application
    3. Assess Credit Worthiness
    4. Evaluate Loan Application
    5. Assess Credit Worthiness

    第二次启动了子流程

    2.6、取消传播

    取消活动实例将传播到不包含其他活动实例的父活动实例。此行为可确保流程实例不会处于毫无意义的执行状态。这意味着,当单个活动在子流程中处于活动状态并且该活动实例被取消时,子流程也会被取消。请考虑以下活动实例树:

    1. ProcessInstance
    2. Decline Loan Application
    3. Evaluate Loan Application
    4. Assess Credit Worthiness

    取消“评估信用价值”的活动实例后,树为:

    1. ProcessInstance
    2. Decline Loan Application

    如果所有指令都已执行,并且没有活动活动实例,则整个流程实例将被取消。在上面的示例中,如果两个活动实例都被取消,一个是评估信用价值,另一个是拒绝贷款申请

    但是,只有在执行完所有指令后,才会取消流程实例。这意味着,如果流程实例在两条指令之间没有活动活动实例,则不会立即取消流程实例。例如,假设活动“拒绝贷款申请”处于活动状态。活动实例树为:

    1. ProcessInstance
    2. Decline Loan Application

    尽管在执行取消指令后,流程实例没有活动活动实例,但以下修改操作将成功:

    1. ProcessInstance processInstance = ...;
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .cancelAllForActivity("declineLoanApplication")
    4. .startBeforeActivity("acceptLoanApplication")
    5. .execute();

    2.7、指令执行顺序

    修改指令始终按提交顺序执行。因此,以不同的顺序执行相同的指令可能会有所不同。请考虑以下活动实例树:

    1. ProcessInstance
    2. Evaluate Loan Application
    3. Assess Credit Worthiness

    假设您的任务是取消“评估信用价值”的实例并启动活动“注册应用程序”。这两条指令有两种排序方式:要么先执行取消,要么先执行实例化。在前一种情况下,代码如下所示:

    1. ProcessInstance processInstance = ...;
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .cancelAllForActivity("assesCreditWorthiness")
    4. .startBeforeActivity("registerApplication")
    5. .execute();

    由于取消传播,子流程实例在执行取消指令时被取消,仅在执行实例化指令时重新实例化。这意味着,在执行修改后,“评估贷款申请”子流程将出现一个不同的实例。与上一个实例关联的任何实体都已被删除,例如变量或事件订阅。

    相比之下,请考虑首先执行实例化的情况:

    1. ProcessInstance processInstance = ...;
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .startBeforeActivity("registerApplication")
    4. .cancelAllForActivity("assesCreditWorthiness")
    5. .execute();

    由于实例化期间的默认祖先选择,以及在这种情况下取消不会传播到子流程实例这一事实,因此子流程实例在修改后与之前相同。将保留相关实体(如变量和事件订阅)。

    2.8、使用中断/取消语义开始活动

    流程实例修改尊重要启动的活动的任何中断或取消语义。特别是,启动中断边界事件或中断事件子流程将取消/中断在中定义的活动。请考虑以下流程:

    假设活动“评估信用价值”当前处于活动状态。事件子流程可以使用以下代码启动:

    1. ProcessInstance processInstance = ...;
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .startBeforeActivity("cancelEvaluation")
    4. .execute();

    由于“取消评估”子流程的启动事件正在中断,因此它将取消“评估信用价值”的运行实例。当事件子流程的启动事件通过以下方式启动时,也会发生同样的情况:

    1. ProcessInstance processInstance = ...;
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .startBeforeActivity("eventSubProcessStartEvent")
    4. .execute();

    但是,当位于事件子流程中的活动直接启动时,不会执行中断。请考虑以下代码:

    1. ProcessInstance processInstance = ...;
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .startBeforeActivity("notifyAccountant")
    4. .execute();

    生成的活动实例树将为:

    1. ProcessInstance
    2. Evaluate Loan Application
    3. Assess Credit Worthiness
    4. Cancel Evaluation
    5. Notify Accountant

    2.9、修改多实例活动实例

    修改也适用于多实例活动。我们在下文中区分了多实例主体和内部活动。内部活动是实际活动,具有流程模型中声明的ID。多实例主体是围绕此活动的一个范围,在流程模型中不作为不同的元素表示。对于id为anActivityId的活动,多实例主体按照约定具有id为anActivity id#multiInstanceBody。

    有了这种区别,就可以启动整个多实例主体,也可以为正在运行的并行多实例活动启动单个内部活动实例。考虑以下流程模型:

    假设多实例活动处于活动状态,并且有三个实例:

    1. ProcessInstance
    2. Contact Customer - Multi-Instance Body
    3. Contact Customer
    4. Contact Customer
    5. Contact Customer

    以下修改将在同一多实例正文活动中启动“联系客户”活动的第四个实例:

    1. ProcessInstance processInstance = ...;
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .startBeforeActivity("contactCustomer")
    4. .execute();

    生成的活动实例树为:

    1. ProcessInstance
    2. Contact Customer - Multi-Instance Body
    3. Contact Customer
    4. Contact Customer
    5. Contact Customer
    6. Contact Customer

    流程引擎确保正确更新与多实例相关的变量nrOfInstances、nrOfActiveInstances和loopCounter。如果基于集合配置多实例活动,则在执行指令时不考虑该集合,并且不会为附加实例填充集合元素变量。这样的行为可以通过使用方法#setVariableLocal向集合元素变量提供实例化指令来实现。

    现在考虑以下请求:

    1. ProcessInstance processInstance = ...;
    2. runtimeService.createProcessInstanceModification(processInstance.getId())
    3. .startBeforeActivity("contactCustomer#multiInstanceBody")
    4. .execute();

    这将再次启动整个多实例正文,从而导致以下活动实例树:

    1. ProcessInstance
    2. Contact Customer - Multi-Instance Body
    3. Contact Customer
    4. Contact Customer
    5. Contact Customer
    6. Contact Customer
    7. Contact Customer - Multi-Instance Body
    8. Contact Customer
    9. Contact Customer
    10. Contact Customer

    2.10、流程实例的异步修改

    可以异步执行单个流程实例的修改。修改指令与同步修改相同,Fluent Builder 的语法如下:

    1. Batch modificationBatch = runtimeService.createProcessInstanceModification(processInstanceId)
    2. .cancelActivityInstance("exampleActivityId:1")
    3. .startBeforeActivity("exampleActivityId:2")
    4. .executeAsync();

    这将创建一个异步执行的修改批处理。 在执行单个流程实例的异步修改时,不支持提供变量。

    2.11、修改多个流程实例

    当有多个流程实例满足特定条件时,可以使用RuntimeService.createModification(…)一次修改它们。此方法允许指定应修改的流程实例的修改指令和ID。流程实例必须属于给定的流程定义。

    fluent修改生成器提供了以下待提交的说明:

    • startBeforeActivity(String activityId)
    • startAfterActivity(String activityId)
    • startTransition(String transitionId)
    • cancelAllForActivity(String activityId)

    可以通过提供一组流程实例 ID 或提供流程实例查询来选择流程实例进行修改。 也可以同时指定流程实例 ID 列表和查询。然后,要修改的流程实例将是结果集的并集。

    1. ProcessInstanceQuery processInstanceQuery = runtimeService.createProcessInstanceQuery();
    2. runtimeService.createModification("exampleProcessDefinitionId")
    3. .cancelAllForActivity("exampleActivityId:1")
    4. .startBeforeActivity("exampleActivityId:2")
    5. .processInstanceIds(processInstanceQuery)
    6. .processInstanceIds("processInstanceId:1", "processInstanceId:2")
    7. .execute();

    可以同步或异步执行多个流程实例的修改。

    同步执行示例:

    1. runtimeService.createModification("exampleProcessDefinitionId")
    2. .cancelAllForActivity("exampleActivityId:1")
    3. .startBeforeActivity("exampleActivityId:2")
    4. .processInstanceIds("processInstanceId:1", "processInstanceId:2")
    5. .execute();

    异步执行示例:

    1. Batch batch = runtimeService.createModification("exampleProcessDefinitionId")
    2. .cancelAllForActivity("exampleActivityId:1")
    3. .startBeforeActivity("exampleActivityId:2")
    4. .processInstanceIds("processInstanceId:1", "processInstanceId:2", "processInstanceId:100")
    5. .executeAsync();

    2.12、跳过侦听器和输入/输出调用

    可以跳过执行和任务侦听器的调用,以及执行修改的事务的输入/输出映射。当在无法访问相关流程应用程序部署及其包含的类的系统上执行修改时,这可能非常有用。可以使用修改生成器的execute方法(boolean skipCustomListeners,boolean skipIoMappings)跳过Listener和ioMapping调用。

    使用注释选项可以出于审核原因传递任意文本注释。

    1. runtimeService.createProcessInstanceModification(processInstanceId)
    2. .cancelAllForActivity("declineLoanApplication")
    3. .startBeforeActivity("processStartEvent")
    4. .annotation("Modified to resolve an error.")
    5. .execute();

    它将在用户操作日志中显示,用于执行的修改。

    2.13、健全性检查

    流程实例修改是一个非常强大的工具,允许随意启动和取消活动。因此,很容易创建正常流程执行无法到达的情况。假设以下流程模型:

    假设活动“拒绝贷款审批”处于活动状态。经过修改后,可以开始活动“评估信用价值”。在该活动完成后,执行被困在加入的并行网关,因为没有令牌会到达其他传入序列流,从而激活并行网关。这是流程实例无法继续执行的最明显情况之一,当然还有许多其他情况,具体取决于具体的流程模型。

    流程引擎无法检测到造成这种情况的修改。此API的用户有权进行修改,使流程实例不会处于不需要的状态。然而,流程实例修改也是修复这些情况的工具

    更详细请参考camunda官方文档:

    https://docs.camunda.org/manual/7.19/user-guide/process-engine/process-instance-modification/

    流程会签实现思路和原理:基于camunda如何实现会签:camunda会签流程配置与原理解析_camunda 会签-CSDN博客

  • 相关阅读:
    会员一卡通是什么?
    RabbitMQ—队列参数
    【Java】封装、继承、多态
    好用、高性能的远程控制软件推荐
    MYSQL06高级_为什么使用索引、优缺点、索引的设计、方案、聚簇索引、联合索引、注意事项
    《Orange‘s 一个操作系统的实现》第二章
    八股文面经整理
    苏州大学:从PostgreSQL到TDengine
    【计算机网络】(6)虚拟机三种网卡模式、SNAT、DNAT
    MyBatis 动态 SQL、MyBatis 标签、MyBatis关联查询
  • 原文地址:https://blog.csdn.net/wxz258/article/details/136361467