ToDo 协作待办
ToDo 在 oak-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 的完整入参语义,最好直接说清楚:
entityfilteractioncontextdatauserIds?
其中 data 当前至少应该准备:
titledescription?redirectToentityentityId
它内部还做了三件非常关键的事:
1. 先查重
它会先按下面这些条件数一遍现有 toDo:
targetEntitytargetFilteractioniState='active'entityentityId
如果已经有同语义的活跃待办,就直接不再创建。
这意味着项目层不需要自己额外做一层“防重复建单”。
2. 不传 userIds 时自动推导协作者
如果你没显式传 userIds,它会调用:
getUserRelationsByActions(...)
自动找出对当前对象在当前 filter 下拥有这个 action 的用户,然后把他们全部挂成 toDo.collaborator。
这也是这套 helper 最大的价值之一:它不是简单待办,而是把 Oak 权限关系直接复用到了协作分派上。
3. 自动补 collaborator 关系
helper 自己会去查 toDo 上名为 collaborator 的 relation,然后创建对应的 userRelation$entity。
所以项目层在大多数场景下,只需要关心:
- 什么时候建待办
- 给谁建待办
不需要自己再手写协作者关系创建逻辑。
后台规则
这里有一个特别容易误会的点:
src/triggers/toDo.ts 这个文件虽然叫 trigger,但它并没有像其它模块那样被自动加入 src/triggers/index.ts。
这意味着:
- 它不是一组默认自动注入的触发器;
- 而是一组需要你在项目层主动调用的 helper。
例如,你可以在项目自己的某个 trigger 里:
- 在需要人工处理时调用
createToDo(...)创建待办; - 在真正完成动作后调用
completeToDo(...)自动关闭待办。
而 createToDo(...) 还有一个很关键的默认行为:
- 如果你没传
userIds,它会调用getUserRelationsByActions(...),自动找出对当前对象拥有该action权限的用户; - 然后把这些用户挂到
toDo的collaborator关系上。
这正是 Oak 很推荐的一种复用方式:公共包提供通用例程,项目层决定在哪些业务节点把它接进去。
completeToDo(...) 的真实完成条件
这个 helper 也不是“按 id 直接把待办置完成”。
它真正做的是:
- 先按
targetEntity + targetFilter + action + iState='active'找出对应活跃待办 - 再去
count(targetEntity, { filter: targetFilter }) - 只有当这个计数已经变成
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-busi、taicang 的源码检索里,没有看到它们直接调用 createToDo(...) / completeToDo(...)。
这更能说明它的定位:
- 它是一组公共协作 helper
- 不是两个示例项目当前正在重度依赖的成品待办模块
所以接这章时,项目团队要自己决定:
- 哪些业务动作值得建待办
- 待办在页面上以什么形式呈现
- 完成条件是“状态变化”还是“对象不再命中过滤条件”
推荐的项目层拆法
最稳的接法通常是:
- 在
before/aftertrigger 里决定什么时候调用createToDo(...) - 在对应完成动作的
aftertrigger 里调用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(...)的filter和completeToDo(...)的filter必须保持同一条业务语义,否则要么重复建单,要么永远完不成。- 如果目标动作只是更新状态,最好让
filter包含“待处理状态”这一层条件,这样完成后count()才会归零。 - 因为 helper 自己不会给你做前端页面,所以项目立项时最好同时规划好列表页、提醒入口和跳转页,不要只建数据不落使用场景。
它不是通用的任务管理系统,而是一组和 Oak 动作模型天然契合的协作待办工具。