Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ToDo 协作待办

ToDooak-general-business 里不是一个完整的“待办系统成品”,而是一套非常适合 Oak 的协作待办辅助模型。

它的核心思想是:

  • 某个对象上有某个动作还没完成;
  • 需要把这件事分配给一批协作者;
  • 当真正执行了目标动作后,对应待办自动完成。

这和一般意义上的“个人待办清单”很不一样。

主要对象

这一章的核心实体是:

  • ToDo

它定义了:

  • 待办标题和描述;
  • 目标对象 targetEntity
  • 目标过滤条件 targetFilter
  • 目标动作 action
  • 完成后应该跳转到哪里;
  • 当前这条待办属于哪个业务对象。

此外,ToDo 自己还内置了:

  • 关系 collaborator
  • 状态 active / done
  • 动作 complete

组件

这一章反而要明确指出:当前 oak-general-business没有现成的 toDo 组件。

这意味着:

  • 你可以直接复用这个实体模型;
  • 但待办列表、待办详情、待办面板通常要在项目层自己补页面或组件。

aspect / endpoint / feature

ToDo 也没有单独的 feature、aspect 或 endpoint。

更准确地说,它在 oak-general-business 里的主要价值其实不在“对外入口”,而在 src/triggers/toDo.ts 里提供的两个辅助函数:

  • createToDo(...)
  • completeToDo(...)

这两个 helper 都是围绕 Oak 的动作模型设计的,不是普通的“新建一条任务、改个状态”。

createToDo(...) 的真实参数与行为

这个 helper 的完整入参语义,最好直接说清楚:

  • entity
  • filter
  • action
  • context
  • data
  • userIds?

其中 data 当前至少应该准备:

  • title
  • description?
  • redirectTo
  • entity
  • entityId

它内部还做了三件非常关键的事:

1. 先查重

它会先按下面这些条件数一遍现有 toDo

  • targetEntity
  • targetFilter
  • action
  • iState='active'
  • entity
  • entityId

如果已经有同语义的活跃待办,就直接不再创建。

这意味着项目层不需要自己额外做一层“防重复建单”。

2. 不传 userIds 时自动推导协作者

如果你没显式传 userIds,它会调用:

  • getUserRelationsByActions(...)

自动找出对当前对象在当前 filter 下拥有这个 action 的用户,然后把他们全部挂成 toDo.collaborator

这也是这套 helper 最大的价值之一:它不是简单待办,而是把 Oak 权限关系直接复用到了协作分派上。

3. 自动补 collaborator 关系

helper 自己会去查 toDo 上名为 collaboratorrelation,然后创建对应的 userRelation$entity

所以项目层在大多数场景下,只需要关心:

  • 什么时候建待办
  • 给谁建待办

不需要自己再手写协作者关系创建逻辑。

后台规则

这里有一个特别容易误会的点:

src/triggers/toDo.ts 这个文件虽然叫 trigger,但它并没有像其它模块那样被自动加入 src/triggers/index.ts

这意味着:

  • 它不是一组默认自动注入的触发器;
  • 而是一组需要你在项目层主动调用的 helper。

例如,你可以在项目自己的某个 trigger 里:

  • 在需要人工处理时调用 createToDo(...) 创建待办;
  • 在真正完成动作后调用 completeToDo(...) 自动关闭待办。

createToDo(...) 还有一个很关键的默认行为:

  • 如果你没传 userIds,它会调用 getUserRelationsByActions(...),自动找出对当前对象拥有该 action 权限的用户;
  • 然后把这些用户挂到 toDocollaborator 关系上。

这正是 Oak 很推荐的一种复用方式:公共包提供通用例程,项目层决定在哪些业务节点把它接进去。

completeToDo(...) 的真实完成条件

这个 helper 也不是“按 id 直接把待办置完成”。

它真正做的是:

  1. 先按 targetEntity + targetFilter + action + iState='active' 找出对应活跃待办
  2. 再去 count(targetEntity, { filter: targetFilter })
  3. 只有当这个计数已经变成 0 时,才会真正执行 toDo.complete

这背后的设计含义很重要:

  • completeToDo(...) 更适合“当某类待处理对象已经不存在”这种语义
  • 如果你的目标动作只是把状态从 A 改成 B,而对象仍然能被原 filter 命中,那待办就不会被自动关闭

所以项目层设计 filter 时,一定要和“完成后这条对象还能不能被查出来”一起考虑。

注入点

这一章的注入点不是 ogb0Triggers,而是你自己的项目代码。

也就是说:

  • ToDo 实体会自动进入项目;
  • createToDo / completeToDo 需要你手工在项目 trigger 里 import 并调用。

这一点如果不说清楚,新手会很容易以为“我依赖了这个包,待办为什么没有自动跑起来”。

项目中如何接入

ToDo 这章的项目接入点非常明确:

  • 在项目自己的 trigger 里 import { createToDo, completeToDo } from '@oak-general-business/triggers/toDo'
  • 在合适的业务动作前后主动调用它们
  • 如果你不想走自动协作者推导,也可以显式传 userIds

它不是自动注入能力,所以项目层必须显式接上。

当前项目里的实际情况

这次对 haina-busitaicang 的源码检索里,没有看到它们直接调用 createToDo(...) / completeToDo(...)

这更能说明它的定位:

  • 它是一组公共协作 helper
  • 不是两个示例项目当前正在重度依赖的成品待办模块

所以接这章时,项目团队要自己决定:

  • 哪些业务动作值得建待办
  • 待办在页面上以什么形式呈现
  • 完成条件是“状态变化”还是“对象不再命中过滤条件”

推荐的项目层拆法

最稳的接法通常是:

  • before / after trigger 里决定什么时候调用 createToDo(...)
  • 在对应完成动作的 after trigger 里调用 completeToDo(...)
  • 页面层自己做 toDo 列表、卡片、提醒角标

因为公共包没有现成 UI,所以项目层页面一般只需要围绕实体 toDo 正常做 Oak 页面即可。

使用示例

1. 在业务 trigger 里创建待办

最小调用骨架通常像这样:

const targetEntity = 'application' as const;
const targetAction: EntityDict['application']['Action'] = 'update';
const targetFilter: EntityDict['application']['Filter'] = {
  id: applicationId,
};

await createToDo(
  targetEntity,
  targetFilter,
  targetAction,
  context,
  {
    title: '请更新应用配置',
    description: '这是一条由 trigger 创建的协作待办',
    redirectTo: {
      batchPath: '/console/application/list',
      singlePath: '/console/application/detail',
    },
    entity: targetEntity,
    entityId: applicationId,
  }
);

如果这里不传最后一个 userIds 参数,helper 会自动按 yourEntity + yourAction 去推导协作者。

1.1 显式指定协作者的写法

如果你不希望 helper 自动推导协作者,也可以直接把用户列表传进去:

await createToDo(
  targetEntity,
  targetFilter,
  targetAction,
  context,
  {
    title: '请更新应用配置',
    redirectTo: {
      batchPath: '/console/application/list',
      singlePath: '/console/application/detail',
    },
    entity: targetEntity,
    entityId: applicationId,
  },
  [reviewerId, operatorId]
);

这更适合协作者是明确固定人选,而不是由权限自动推导的流程。

2. 在动作完成后的 after trigger 里关闭待办

调用时要保证传入的 entity / filter / action 和创建待办时保持同一条业务语义:

await completeToDo(
  targetEntity,
  targetFilter,
  targetAction,
  context
);

对新手来说,最重要的是记住:completeToDo(...) 设计上就应该放在对应动作的后置 trigger 里调用。它不是“按 id 直接关单”,而是会先找匹配 targetFilter 的活跃待办,再检查目标对象在这个过滤条件下是否已经不存在,只有这样才会真正执行 complete

使用建议

最适合 ToDo 的场景,是那些本来就有明确目标动作的业务对象,例如:

  • 某条记录需要审核;
  • 某个流程需要确认;
  • 某个动作需要协作者去完成。

再补三条非常关键的实战建议:

  • createToDo(...)filtercompleteToDo(...)filter 必须保持同一条业务语义,否则要么重复建单,要么永远完不成。
  • 如果目标动作只是更新状态,最好让 filter 包含“待处理状态”这一层条件,这样完成后 count() 才会归零。
  • 因为 helper 自己不会给你做前端页面,所以项目立项时最好同时规划好列表页、提醒入口和跳转页,不要只建数据不落使用场景。

它不是通用的任务管理系统,而是一组和 Oak 动作模型天然契合的协作待办工具。