rails-engine / flow_core

FlowCore is a Rails engine to help you build your automation or business process application.
MIT License
172 stars 17 forks source link

flow_core 集成问题 #6

Open dnnta opened 4 years ago

dnnta commented 4 years ago

您好,咨询下 flow_core 集成已有业务系统的问题。 现有业务系统是通过 FSM 来进行简单状态管理的,现在想集成这个Gem 满足知会等功能。

但是文档里提到

审批工作流的特点是流程绑定一个表单...

翻看了源码,work_flow, instance, task 基本都关联了 form,且必须 form_filled 后才能 finish。现需要集成的效果是,现有的业务系统只需要在表单 save 时启动这个工作流实例,接收审核结果就行。因此 work_flow 只需要关联已存在的业务 model 就行。比如我们现有的采购流程,希望订单创建保存后,启动 work_flow,关联这个订单ID就行。

demo 里的工作流感觉有点怪,感觉是为了 work_flow 而创建了一个业务 form,而不是先有业务,再去跑的工作流。(之前基本没搞过工作流,这个感觉错了勿怪==~)。

该怎么集成呢,是把业务 model 转 FormKit:Form,适配 flow_kit, 还是对照 flow_kit 按照自己的业务重新撸一份呢

jasl commented 4 years ago

你的需求其实也是典型场景,类似 Jira Redmine 之类的工作流都是这样用的。

是这样,其实早年的 Dummy 是演示了一个请假流程,使用一个写死的请假记录的模型,然后工作流关联,后来感觉这个用例不够直观,就给换掉了,换成现在的审批流程。

改版前的最后一个提交是 https://github.com/rails-engine/flow_core/tree/b6404c8bdb18a41de260c07001c1e44bbd9103e6 (注:那个版本还没引入 Pipeline API)

当时那个版本的代码放到现在应该依然可用(Workflowon_ 系列钩子函数去掉了,因为 Instance 现在支持 STI),简单讲一下:

首先有一个请假记录模型 app/models/leave.rb

然后接入 Workflow,有两种做法:

创建 LeaveInstance 也就是请假的工作流实例类型,设置跟请假记录的关联(记得添加 migration add_references :flow_core_instances, :leaves)

class LeaveInstance < FlowCore::Instance
  belongs_to :leave
end

class LeaveWorkflow < FlowCore::Workflow
  belongs_to :leave

  private

    def on_build_instance(instance)
      # 这里是创建流程实例的 hook 方法,你可以在这里根据业务需要任意定制
      instance.leave = leave
    end

    def instance_class
      LeaveInstance
    end
end

即可,如果你需要 Pipeline 方便编辑流程,那么多加一个

class LeavePipeline < FlowCore::Pipeline
  belongs_to :leave

  private

    def on_build_workflow(workflow)
      workflow.leave = leave
    end

    def workflow_class
      LeaveWorkflow
    end
end

就可以了,这样好处是扩展代码容易管理,流程有类型容易过滤,但是繁琐一点

另一种偷懒,你不为请假去定义流程的子类,你可以把流程要关联的请假记录的 id 塞进 Instancepayload 中去,初始化流程的代码类似这样去写

FlowCore::Workflow.create_instance payload: { leave_id: @leave.id }

然后可以在 Leave 模型中增加外键 belongs_to :instance (类似 app/models/leave.rb#L6 这样)

如果你还是需要审批意见的话,不需要动态表单这样复杂的话,你可以修改我 Dummy 里的 HumanTask,就让他关联一个你定义死的 AttachedForm (指代审批意见表单或者答复表单),就可以了

这样渲染流程步骤的页面的时候,审批数据(就是请假记录)直接从关联中读取即可

我觉得你也可以这样理解,每一个 HumanTask 都是一个独立的 CRUD 型的任务记录(类似一条需要处理的消息),工作流的作用是根据流程所处的步骤去生成 HumanTask 记录,不知道这样类比你能否理解

jasl commented 4 years ago

这时候 HumanTask 的代码就比较简单了,类似

class HumanTask < FlowCore::ApplicationRecord
  include FlowCore::TaskExecutable

  belongs_to :workflow, class_name: "FlowCore::Workflow"
  belongs_to :instance, class_name: "FlowCore::Instance", autosave: true

  # 请假记录
  belongs_to :leave, validate: true, autosave: true
  # 对请假的审批意见
  has_one :leave_approval, validate: true, autosave: true

  # 这里配合定义关联时的 `validate: true, autosave: true` 做界面的时候可以用嵌套表单去编辑原始请假记录(如果有需要的话)和审批意见,
  # 并且会做验证,如果关联的记录数据有错,依然不让保存
  accpets_nested_attributes :leave
  accpets_nested_attributes :leave_approval

  belongs_to :assignable, polymorphic: true

  enum status: {
    unassigned: "unassigned",
    assigned: "assigned",
    form_filled: "form_filled", # 可选状态了
    finished: "finished"
  }

  before_validation on: :create do
    if task
      self.workflow = task.workflow
      self.instance = task.instance
    end
  end

  after_create do
    create_leave_approval! # 创建审批意见记录
  end

  def can_finish?
    form_filled? && leave.valid? && (!leave_approval || leave_approval.valid?)
  end

  def can_assign?
    unassigned?
  end

  def can_fill_form?
    assigned? || form_filled?
  end

  def assign!(assignee)
    return unless can_assign?

    update! assignee: assignee, status: :assigned, assigned_at: Time.zone.now
  end

  # 提交完表单保存后,手动在控制器调用下,或者去掉这个状态也可以的
  def fill_form!
    return unless can_fill_form?

    self.status = :form_filled
    self.form_filled_at = Time.zone.now

    save
  end

  def finish!
    return unless can_finish?

    self.status = :finished
    self.finished_at = Time.zone.now

    save!
  end
end

这里因为你流程和审批意见表单都是物理模型了,就没必要存到 InstanceTaskpayload 去了,不过要注意一个是,Dummy 里演示的基于 mruby 的 ArcGuard 设计是从 Taskpayload 里读数据的然后做判断的,你如果觉得对你有用,那么还是要在 finish! 方法里把用于分支判断的字段回写到 Taskpayload 里去

jasl commented 4 years ago

我接下来翻新 Dummy 的时候 用 任务管理 这个场景做一个类似你需求的例子吧,不知道我回复的你能否理解?

dnnta commented 4 years ago

回答的很 nice,非常感谢。

jasl commented 4 years ago

周末更新了一下,去掉了 TransitionCallback 想了一些场景,发现没啥价值,这样还能少理解一个概念。

做通知任务处理人,直接在 HumanTask#assign! 里加一个 assignee.notifications.create 就好了,这需求简单的要死。

没推到rubygems上,master 已经做了,应该不会影响到现在版本的使用。

dnnta commented 4 years ago

为你的勤奋点赞!

已经用你之前的指点在尝试了,现在有个问题是如果有多种工作流的话,HumanTask可能也要做个多态,不然不同类型的工作流的状态变化逻辑都在一个类处理好像行不通。或者应该是在 TransitionCallback 里处理把。

jasl commented 4 years ago

为你的勤奋点赞!

已经用你之前的指点在尝试了,现在有个问题是如果有多种工作流的话,HumanTask可能也要做个多态,不然不同类型的工作流的状态变化逻辑都在一个类处理好像行不通。或者应该是在 TransitionCallback 里处理把。

我觉得你可以在 HumanTask 上做 STI,然后不同工作流用 HumanTask 的子类,偷懒可以这样修改 HumanTrigger,Dummy 的例子 app/models/flow_kit/transition_triggers/human_task.rb#L35 这里,简单修改成:

def on_task_enable(task)
  transaction do
    assignee =
      case configuration.assign_to
      when Configuration::ASSIGN_TO_ENUM[:candidate]
        assignee_candidates.order("random()").first&.assignable
      when Configuration::ASSIGN_TO_ENUM[:instance_creator]
        task.instance&.creator
      else
        raise "Invalid `assign_to` value - #{configuration.assign_to}"
      end

    # 这里根据工作流的类型来决定创建哪种任务
    human_task_type = 
      case task.workflow
      when ApprovalWorkflow
        "ApprovalHumanTask"
      when BusinessWorkflow
        "BusinessHumanTask"
      else
        raise "Unhandled workflow type - #{task.workflow.class}"
      end

    # `type: human_task_type` 这里利用 STI
    human_task = task_class.create! type: human_task_type, task: task, attached_form: attached_form, form_override: form_override, status: :unassigned
    human_task.assign! assignee
  end
end

如果任务区别特别大的话,也可以做不同的模型,为区别很大的任务模型写对应的 Trigger,或者偷懒上边的这个写法应该也可以适用。

dnnta commented 4 years ago

正是这样做的👍