Skip to content

任务管理平台技术方案 ​

对应 PRD 原文。本文定义数据模型、MVP 状态机和核心流程;两者冲突时以 PRD 为准并回头修本文。 需求来源:lamolabs/lamolabs-docs#48。

1. 数据模型与查询策略 ​

1. ER 总览 ​

2. 实体字段明细 ​

2.1 组织与用户 ​

ORGANIZATION(组织) ​
字段类型说明
idPK组织 ID
namestring组织名称
created_atdatetime创建时间

最高隔离边界,跨组织严禁访问(PRD 4.2.1)。

TEAM(团队) ​
字段类型说明
idPK团队 ID
org_idFK → ORGANIZATION所属组织
namestring团队名称
created_atdatetime创建时间

组织下协作单元,跨团队任务默认隔离(PRD 4.2.1)。

ITERATION(迭代排期) ​
字段类型说明
idPK迭代 ID
org_idFK → ORGANIZATION所属组织
team_idFK → TEAM所属团队
namestring迭代名称
starts_atdatetime, nullable开始时间
ends_atdatetime, nullable结束时间
created_atdatetime创建时间

迭代属于团队,任务通过 iteration_id 关联;同一组织跨团队的排期仍分别维护,避免把不同团队的迭代混为一谈。

USER_ORG_MEMBERSHIP(用户-组织归属) ​
字段类型说明
user_idFK → USER用户
org_idFK → ORGANIZATION组织
membership_typeenummember / external_guest
statusenumactive / suspended
joined_atdatetime加入时间
(PK)(user_id, org_id)

所有用户必须先有有效的组织归属;外部访客可以不加入任何团队,但只能访问被授权的外部任务。负责人、创建人、提交人和参与人都必须通过该表校验组织边界。

USER(用户) ​
字段类型说明
idPK用户 ID(来自认证中心)
namestring姓名
kindenumhuman / service;平台系统服务账号使用 service
created_atdatetime创建时间

用户是认证中心的全局身份;是否为内部成员或外部访客由 USER_ORG_MEMBERSHIP.membership_type 按组织决定,不使用全局 is_external 标志。与团队多对多,外部访客可不加入团队(PRD 4.2.1、6.4)。kind=service 的平台系统服务账号必须以 active 的内部成员归属绑定到目标组织,只能作为后台操作主体,不能作为外部提交人、负责人或任务参与人。

USER_TEAM(用户-团队关系) ​
字段类型说明
user_idFK → USER用户
team_idFK → TEAM团队
roleenum该用户在此团队的角色:member / team_admin
joined_atdatetime加入时间
(PK)(user_id, team_id)

只有 USER.kind=human 且 USER_ORG_MEMBERSHIP.membership_type=member、状态为 active 的用户才能加入团队;service 账号不得写入 USER_TEAM,也不能成为 team_admin。组织管理员为组织级角色,单独存于 USER_ORG_ROLE(见下)。

USER_ORG_ROLE(用户-组织角色) ​
字段类型说明
user_idFK → USER用户
org_idFK → ORGANIZATION组织
roleenumorg_admin
(PK)(user_id, org_id)

组织角色从架构角度划分:成员 / 团队管理员 / 组织管理员(PRD 4.2.3)。该记录必须对应 USER.kind=human、active 且 membership_type=member 的 USER_ORG_MEMBERSHIP;service 账号和外部访客不能获得 org_admin。"任务负责人"是任务级属性,不在此表。

ROLE(组织级任务角色) ​
字段类型说明
idPK角色 ID
org_idFK → ORGANIZATION所属组织
codestring角色编码,如 developer / tester
namestring角色名称
created_atdatetime创建时间

这是任务类型可见性矩阵使用的功能角色目录,与 member / team_admin / org_admin 组织角色分离。MVP 预置保留角色 member 以及功能角色 developer、tester,后续可由组织管理员扩展。

USER_ROLE(用户-任务角色关系) ​
字段类型说明
idPK分配记录 ID
user_idFK → USER用户
role_idFK → ROLE任务角色
team_idFK → TEAM角色生效的团队
created_atdatetime分配时间
(UQ)(user_id, role_id, team_id)

PermissionService 按任务所属团队读取用户的任务角色;visibility_scope=cross_team 时,再按 visible_team_ids 读取这些团队中的用户角色,与任务绑定的 TASK_TYPE_VERSION.visible_roles 做匹配。一个用户可在不同团队拥有不同任务角色。活动 USER_TEAM 关系自动派生保留任务角色 member,不要求额外写入 USER_ROLE;USER_ROLE 只记录 developer / tester 等额外功能角色。

2.2 任务类型(组织级,完全可配置) ​

TASK_TYPE(任务类型) ​
字段类型说明
idPK类型 ID
org_idFK → ORGANIZATION所属组织(组织级共享)
namestring类型名(如 需求/开发任务/外部工单)
default_version_idFK → TASK_TYPE_VERSION, nullable组织级默认的已发布配置版本;无团队覆盖时使用
external_default_team_idFK → TEAM, nullableexternal 类型的稳定默认处理团队;在选择类型版本前确定路由,外部提交人不能传入
external_handler_user_idsjson, nullableexternal 类型的稳定处理方白名单;必须全部属于 external_default_team_id,发布版本时复制为版本快照
created_atdatetime创建时间

TASK_TYPE 保存稳定身份和外部工单的固定路由;可见范围、角色矩阵、字段、状态机和关联声明归档在不可变的 TASK_TYPE_VERSION。管理员修改类型时创建并发布新版本,不能原地改写已有版本。external_default_team_id / external_handler_user_ids 是外部路由的稳定来源,发布版本时复制为快照并要求一致;因此外部任务可以先确定处理团队,再按该团队选择启用版本。MVP 为需求、开发任务、外部工单各发布组织级 v1;每个团队可通过 TASK_TYPE_ACTIVE_VERSION 选择自己的已发布版本,没有团队覆盖时使用默认版本。

TASK_TYPE_VERSION(任务类型配置版本) ​
字段类型说明
idPK配置版本 ID
type_idFK → TASK_TYPE所属任务类型
team_idFK → TEAM, nullable生效团队;NULL 表示组织级默认版本
versionint同一类型、同一生效范围内递增的版本号
visibility_scopeenum可见范围:internal / external / cross_team
visible_rolesjson可见角色 ID 列表(引用 ROLE,角色 × 类型可见性矩阵)
visible_team_idsjson, nullablecross_team 类型允许查看的组织内团队 ID 列表;必须包含任务所属团队,其他范围必须为空
external_default_team_idFK → TEAM, nullable从 TASK_TYPE.external_default_team_id 复制的版本快照;external 类型必填且必须与稳定路由一致
handler_user_idsjson, nullable外部工单的处理方用户 ID 白名单版本快照,仅管理员可配置/读取;external 版本必须非空且全部属于 external_default_team_id
form_schemajson资源/表单字段定义(按类型配置);每个字段必须声明 storage=form_data 或 storage=task_resource,并声明 visibility=metadata / shared / internal。仓库、PR、文档、链接等资源型字段只能写入 TASK_RESOURCE,普通自定义字段只能写入 TASK.form_data
node_overrides_form_schemasjson节点级实例参数的表单 schema(json 数组),驱动 TASK.node_overrides 渲染。结构:[{"schema_id": "human_gate_approver", "label": "人工卡点配置", "fields": [{"key": "approver_id", "label": "审批人", "type": "user_select", "required": true}], "applies_to": ["node_id_1", "node_id_2"]}]。一份 schema 可复用多个节点(applies_to),新增节点只需加 ID,零代码改动
relation_declarationsjson可声明的关联关系及联动规则;cross_type 必须预先写入固定的 target_team_id 与 target_task_type_id(MVP 不从外部提交输入推断目标团队或类型)
statusenumdraft / published / retired
created_atdatetime创建时间
(UQ-ORG)(type_id, version) WHERE team_id IS NULL;组织级默认版本的唯一约束
(UQ-TEAM)(type_id, team_id, version) WHERE team_id IS NOT NULL;团队级版本的唯一约束

published 版本及其 STATE_NODE / STATE_TRANSITION 不可编辑、删除或复用版本号;新任务按所属团队读取 TASK_TYPE_ACTIVE_VERSION,没有团队覆盖时回退组织级默认版本,进行中的任务继续使用自己绑定的旧版本。 上述两条约束必须以数据库部分唯一索引(或等价的条件唯一约束)落地,不能依赖包含可空 team_id 的普通复合唯一键,否则多条 team_id IS NULL 的组织级版本可能同时插入。

TASK_TYPE_ACTIVE_VERSION(团队启用的任务类型版本) ​
字段类型说明
type_idFK → TASK_TYPE任务类型
team_idFK → TEAM生效团队
version_idFK → TASK_TYPE_VERSION当前团队使用的已发布版本
(PK)(type_id, team_id)

同一团队同一任务类型只能启用一个版本;version_id 必须属于同一 type_id 且 TASK_TYPE_VERSION.team_id 等于该团队,不能把其他团队或组织级版本误挂到团队覆盖记录。外部类型的团队覆盖版本仍必须使用 TASK_TYPE.external_default_team_id 作为处理团队,不能改变外部路由。

STATE_NODE(状态节点,任务类型版本的状态机) ​
字段类型说明
idPK节点 ID
type_version_idFK → TASK_TYPE_VERSION所属任务类型配置版本
namestring状态名
exit_modeenum默认离开方式:manual(手动推进)/ auto(外部事件触发)/ human_gate(人工卡点)/ terminal(终态)
auto_triggerenum, nullable默认自动触发器(默认 exit_mode=auto 时必填):pr_merged / children_done / related_task_done / ...
gate_policyenum, nullable人工卡点策略:configured_approver / submitter_only,仅 human_gate 使用
is_initialbool是否为该配置版本的初始节点;每个 TASK_TYPE_VERSION 必须且只能有一个
sortint顺序
(PARTIAL UQ)type_version_id WHERE is_initial=true,保证每个版本只有一个初始节点

节点的 exit_mode 是默认值和能力声明;实际每条出边的 trigger_mode / auto_trigger / operator_policy / guard 以 STATE_TRANSITION 为准。human_gate 节点的审批人仍在任务实例层配置(见 TASK.node_overrides),但 submitter_only 不允许管理员代审。

STATE_TRANSITION(状态流转规则) ​
字段类型说明
idPK规则 ID
from_node_idFK → STATE_NODE前置状态
to_node_idFK → STATE_NODE后置状态
labelstring, nullable出边标签(如 human_gate 的"通过"/"驳回")
trigger_modeenum该出边的触发方式:manual / auto / human_gate
auto_triggerenum, nullabletrigger_mode=auto 时的触发器:pr_merged / children_done / related_task_done / ...
operator_policyenum操作者策略:default / admin_recovery;管理补偿只能用于预先声明的人工补偿边
guardenum边前置条件:none / no_children / children_done / all_linked_prs_merged / related_task_done / admin_recovery_pending;无前置条件统一存 none,禁止使用 NULL
(UQ)(from_node_id, to_node_id, trigger_mode, auto_trigger, label, guard),按 NULLS NOT DISTINCT 语义处理仍可空的 auto_trigger / label
(PARTIAL UQ)(from_node_id, auto_trigger, guard) WHERE trigger_mode='auto',同一自动条件只能有一个目标出边

每条出边都显式声明触发方式、自动触发器、操作者策略和 guard;运行时唯一读取 STATE_TRANSITION,节点 exit_mode / auto_trigger 只用于配置编辑器默认值,保存配置时必须物化到每条出边并校验一致。每个配置版本必须且只能有一个 is_initial=true 节点;terminal 节点不能作为 from_node_id,trigger_mode=auto 时必须填写 auto_trigger,operator_policy=admin_recovery 只能用于 trigger_mode=manual、guard=admin_recovery_pending 且不能把任务送入终态或绕过 submitter_only;guard=admin_recovery_pending 还必须验证关联任务已完成且对应事件未成功应用,不能由管理员仅凭按钮强行推进;guard=no_children 只有在没有任何子任务时可通过,guard=children_done / related_task_done 必须在同一事务中验证完成条件;guard=all_linked_prs_merged 必须要求至少一个 active PR 资源且所有 active PR 均已合并;from_node_id / to_node_id 必须属于同一配置版本。数据库必须使用 NULLS NOT DISTINCT 唯一约束,或使用生成列/唯一表达式把 NULL 映射到保留哨兵值;不支持这两类能力时,由同一事务内的领域校验拒绝重复组合,不能依赖普通 SQL UNIQUE 的 NULL 语义。

2.3 任务核心 ​

TASK(任务) ​
字段类型说明
idPK任务 ID
team_idFK → TEAM所属团队
org_idFK → ORGANIZATION所属组织(冗余,便于组织级查询)
type_idFK → TASK_TYPE任务类型
type_version_idFK → TASK_TYPE_VERSION不可变的任务类型配置版本;新任务取当前已发布版本,存量任务不随类型编辑漂移
iteration_idFK → ITERATION, nullable所属迭代排期;必须与任务团队属于同一组织/团队
titlestring, nullable when archived标题
descriptiontext, nullable when archived描述
assignee_idFK → USER, nullable负责人(唯一);外部工单处于“待处理”时允许为空,处理方领取时原子写入
current_state_idFK → STATE_NODE, nullable when archived当前主状态;归档存根不再保留
root_task_idFK → TASK(延迟约束)顶层任务 ID(冗余且非空),无父任务时必须 = 自身 id;提升查询性能,避免 DAG 递归(PRD 7.6)。该自引用外键使用 DEFERRABLE INITIALLY DEFERRED,在事务提交时再校验
node_overridesjson, nullable任务层面对节点的补充信息(实例级参数):{"node_id": {"approver_id": "user_123"}, ...},key = 节点 ID,value = 该节点在此任务实例下的参数。当前用于 human_gate 审批人,后续可扩展任意节点级实例参数
form_datajson, nullable when archived按类型 form_schema 填写的自定义字段值
statusenumactive / closed / archived
closed_atdatetime, nullable关闭时间(gap 期起算点)
reopen_state_idFK → STATE_NODE, nullable关闭时记录的恢复目标,默认取关闭前最后一个非终态节点
archived_atdatetime, nullable归档时间(迁离线表)
archive_refstring, nullable归档详情记录的稳定引用;仅 status=archived 时填写
versionbigint状态与关键字段的乐观锁版本,每次成功更新递增
due_atdatetime, nullable规范化截止时间
created_byFK → USER, nullable when archived创建人
submitter_idFK → USER, nullable仅外部任务必填的提交人;用于外部内容可见性和 submitter_only 卡点,不传播到关联内部任务
created_atdatetime, nullable when archived创建时间
updated_atdatetime, nullable when archived更新时间

模型不区分主/子任务,全部平等(PRD 6.1.1)。root_task_id 冗余 + 用户端内存组装(PRD 7.6)。 归档任务迁至离线表 TASK_ARCHIVE(结构同 TASK,PRD 6.1.7)。

创建事务必须先生成 TASK.id,再写入非空 root_task_id;无父任务时写自身,有父任务时写父任务的根。root_task_id → TASK.id 的自引用外键必须是 DEFERRABLE INITIALLY DEFERRED(或数据库等价的提交时约束),否则根任务插入时被引用的 TASK 行尚不存在会触发外键失败。due_at 和 iteration_id 供所有任务类型共用的时间视图与筛选使用,并建立 (team_id, iteration_id, due_at)、(org_id, due_at)、(assignee_id)、(created_by) 与 (submitter_id) 索引。

在线存根约束:归档不删除 TASK 行,因为在线关系和资源仍需通过任务 ID 指向它;TASK 行保留 id、team_id、org_id、type_id、type_version_id、root_task_id、status、closed_at、archived_at、archive_ref 与 version,其余详情列在 status=archived 时允许置空。数据库需增加条件检查:status=archived 时 closed_at、archived_at、archive_ref 必须非空,title、description、current_state_id、form_data、created_by、created_at、updated_at 等详情列必须为空;非归档任务则必须保留这些创建、状态和内容字段,且 archived_at、archive_ref 为空。完整字段(包括标题、描述、表单数据、负责人、创建人、提交人和时间字段)写入 TASK_ARCHIVE,archive_ref 指向该归档记录;所有读取归档详情的请求必须按权限投影从 TASK_ARCHIVE 组装,不能从在线存根恢复敏感内容。

跨实体一致性约束 ​

以下约束在数据库复合外键可用时优先落成复合外键;否则必须由同一事务内的领域校验保证,不能只依赖各自的单列外键:

  • TASK.org_id = TEAM.org_id = TASK_TYPE.org_id;负责人、创建人和参与人必须属于同一组织。内部任务的负责人和参与人必须是该组织内 active 的 membership_type=member;外部任务的负责人(如已领取)和参与人必须是 active 内部成员。created_by 可以是同组织的人类用户或已绑定该组织的 service 账号;外部任务的 submitter_id 必须是该组织内 active 的 external_guest,内部任务必须为 NULL。
  • 直接由外部提交人创建工单时,created_by 与 submitter_id 可以指向同一人,但两者语义和授权来源仍独立;由工单联动创建内部任务时,内部任务只记录 service 账号的 created_by,不得继承外部提交人的身份。
  • visibility_scope=cross_team 的版本必须填写非空 visible_team_ids,列表中的团队必须全部属于同一组织且包含任务所属团队;查看者必须是列表内团队的 active 内部成员,并在其生效团队拥有版本 visible_roles 中的角色。internal / external 版本的 visible_team_ids 必须为空,外部任务不得使用 cross_team。
  • TASK_TYPE.external_default_team_id 仅允许指向同组织团队;visibility_scope=external 时必须非空,external_handler_user_ids 必须非空且每个用户属于该团队。发布的外部 TASK_TYPE_VERSION 必须复制并保持同一 external_default_team_id;其 handler_user_ids 必须非空且每个用户属于该团队。创建外部任务时先读取稳定的 TASK_TYPE.external_default_team_id 确定 TASK.team_id,再选择该团队的 TASK_TYPE_ACTIVE_VERSION 或组织默认版本,不能接受外部提交人传入的任意团队。
  • 外部任务在“待处理”状态允许 assignee_id=NULL;处理方领取必须锁定任务、校验自己属于 TASK_HANDLER 白名单并在同一事务写入负责人和版本,离开“待处理”后负责人不得为空。
  • 外部提交入口只接受标题、描述和版本允许的 shared 表单字段;客户端传入的 team_id、assignee_id、TASK_PARTICIPANT 或 TASK_HANDLER 一律拒绝(或在审计后丢弃),这些处理方字段只能由内部管理员配置或白名单处理方领取。
  • TASK.type_version_id 必须属于 TASK.type_id;仅在创建任务时按所属团队的 TASK_TYPE_ACTIVE_VERSION 解析,无团队覆盖时回退 TASK_TYPE.default_version_id,并将解析结果绑定到任务。后续 active/default 版本切换不得阻断存量任务继续使用其不可变版本;current_state_id 与 reopen_state_id 必须属于同一 type_version_id,不能跨版本恢复。
  • STATE_TRANSITION.from_node_id 与 to_node_id 必须属于同一 TASK_TYPE_VERSION;每个版本恰有一个 is_initial=true 节点,终态节点不得有出边。
  • TASK_RELATION 的两端必须属于同一组织;mount_state_id 必须属于源任务绑定的 TASK_TYPE_VERSION,关联类型和联动规则必须命中该版本的 relation_declarations。TASK_RELATION_REQUEST.mount_state_id(parent_child 必填、cross_type 必须为空)也必须属于 source 任务绑定的版本,并在确认事务中重新校验。cross_type 的 target 必须命中声明中的固定 target_team_id / target_task_type_id,不能由外部提交数据决定。
  • 任何会改变 children_done、related_task_done 或 all_linked_prs_merged 判断结果的关系、任务终态或 PR 资源写入,都必须先按同一锁序锁定承载流转的 flow_task_id,并递增其 TASK.version,再与状态流转共用同一事务执行 guard 检查和 outbox 唤醒;不能让关系/资源写入与状态推进各自通过独立快照判断。
  • 建立、替换或解除关系时,发起申请的一方只需先通过自己一端任务的关系操作权限:establish 使用 canPerformAction(requester, initiator_task, establish_relation),replace / revoke 使用对应的修改或解除关系权限;不能因为默认不可见而要求同一请求者先通过另一端权限。跨团队或请求者无法同时看到两端时,先生成 TASK_RELATION_REQUEST,只向发起端的对侧任务负责人/创建人或对侧团队管理员展示按 getTaskProjection 过滤后的最小元数据;若任一端是外部任务,申请视图只能展示任务 ID、类型、所属团队、状态和关系方向,不得展示标题、描述、表单字段、评论、提交人或处理方信息。对侧确认事务再分别重校验请求者对 initiator 端、确认人对 confirmer_task_id 的当前对应关系操作权限,并在同一事务按 operation 创建、替换或撤销关系及必要授权。组织管理员可直接创建,但任何流程都不能通过关系本身反向扩大目标任务可见性。
  • TASK_TYPE.default_version_id 必须指向同一类型、team_id=NULL 的 published 版本;TASK_TYPE_ACTIVE_VERSION.version_id 必须指向同类型、同团队的 published 版本。已被任务引用的版本只能转为 retired,不能编辑、删除或复用版本号。
  • TASK_ACCESS_GRANT.relation_id 只能指向跨团队内部任务的 parent_child 关系;外部任务和 cross_type 关系不得产生通用隐式授权。
  • TASK_HANDLER.user_id 必须是任务所属组织内的有效内部成员;grant_scope=version_default 必须命中任务绑定版本的 handler_user_ids,grant_scope=task_override 仅允许管理员为存量 active 任务临时迁移处理方,并必须有未过期的 expires_at 与审计原因;外部访客不可成为处理方。
  • USER_ROLE.user_id 必须是 kind=human 且 active 的内部成员,并存在同团队的 active USER_TEAM 关系;USER_ROLE.team_id、ROLE.org_id 与所属团队的组织必须一致。角色写入和每次权限查询都必须重新校验这些成员关系,不能让离队、停用用户或外部访客保留任务类型可见性。
  • active 的 USER_TEAM 成员自动满足保留任务角色 member;team_admin 同样派生 member,不要求额外的 USER_ROLE 行。外部访客不可通过该派生规则获得内部任务角色,也不能被授予 org_admin。
  • 外部工单同步创建的内部任务必须使用平台 service 账号作为 created_by,submitter_id 必须为 NULL;不得把外部提交人写入内部任务的负责人、参与人或隐式授权。
  • service 账号不具备通用创建任务或推进任务权限;只有外部类型版本的 relation_declarations 命中固定目标团队和目标类型时,才允许以 create_linked_task 作为受约束的系统操作主体创建内部任务,并在同一事务写入关系、审计和必要事件。该操作必须经过 PermissionService 的系统策略校验,不能由客户端伪造。
  • 外部任务的 assignee_id 和 TASK_PARTICIPANT 必须命中该任务的 active TASK_HANDLER 白名单;白名单用户每次读取完整内容时仍须是 active 内部成员且属于任务团队。TASK_PARTICIPANT 不能单独授予外部任务完整内容可见性,完整内容只对当前 submitter_id 和满足上述条件的处理方返回。

跨团队只表示同一组织内的协作,绝不放宽组织边界。任何一致性校验失败都拒绝写入并通过独立事务记录 SYSTEM_AUDIT(已有任务把任务 ID 写入 resource_id);只有实际提交成功的任务变更才写入 AUDIT_LOG,不产生半条关联或半条任务。

数据投影与敏感字段保护 ​
  • form_schema 的字段可见性是字段级契约:metadata 对管理员管理视图可见,shared 对外部提交人和处理方可见,internal 只对内部处理方可见;外部任务的团队/组织管理员即使拥有管理权限,也只能走受限管理投影读取任务 ID、类型、状态、团队、负责人和时间,不能读取任何客户字段、评论正文或内部 form_data。外部提交人的身份只从当前外部任务的 submitter_id 读取,不从 created_by 或关联任务反推。
  • PermissionService 除 canViewTask、canPerformAction、canApproveGate 外,还必须提供 canViewComment、getTaskProjection、getCommentProjection 与 getAuditProjection;接口先判定任务/评论权限,再按字段级可见性返回投影,禁止把原始 form_data 或评论正文直接序列化给调用方。
  • 外部工单管理员的投影只允许任务 ID、类型、状态、团队、负责人和时间;客户联系方式、反馈正文、截图、外部评论以及敏感字段的审计前后值统一返回 [REDACTED],审计仍保留字段路径和“已变更”事实。
  • NOTIFICATION.payload 只允许来自元数据白名单,不得携带 form_data 敏感值或评论正文;通知内容需要正文时按收件人权限重新读取。
TASK_PARTICIPANT(任务相关人员) ​
字段类型说明
task_idFK → TASK任务
user_idFK → USER相关人员(可跨团队)
(PK)(task_id, user_id)
(INDEX)(user_id, task_id),支持我的视图按参与人查询

触发单任务级可见性授权,可突破类型/团队隔离(PRD 4.2.2)。创建人、外部工单 submitter_id 和关系驱动的隐式授权不重复写入此表;内部任务的参与人必须是 active 内部成员,外部任务的参与人还必须命中 TASK_HANDLER;外部任务的提交人授权不传播到 cross_type 对侧任务,参与人记录也不能绕过外部任务处理方白名单。

TASK_HANDLER(外部任务处理方) ​
字段类型说明
task_idFK → TASK外部工单
user_idFK → USER处理方用户
assigned_byFK → USER指派人
statusenumactive / revoked;仅 active 记录可参与完整内容授权
grant_scopeenumversion_default / task_override;任务级例外仅用于存量任务处理方迁移
expires_atdatetime, nullabletask_override 的失效时间;默认版本授权为空
grant_reasonstring, nullable任务级例外的审计原因;task_override 时必填
revoked_atdatetime, nullable撤销时间
created_atdatetime授权时间
(PK)(task_id, user_id)

创建外部工单时,从任务绑定版本的 TASK_TYPE_VERSION.handler_user_ids 校验并物化 grant_scope=version_default 的处理方白名单;只有 active 的 TASK_HANDLER 且用户仍是 active 内部成员、仍属于任务团队的用户,才可领取未分配工单、执行“待处理→处理中”,并读取完整外部内容。为处理存量任务的人员迁移,团队/组织管理员可以在单个 active 任务上新增 grant_scope=task_override 的临时处理方,必须填写 grant_reason 和 expires_at,并在同一事务完成原子改派;该例外只作用于该任务,不改变不可变的类型版本,也不能作为新任务的默认白名单。处理方只能操作自己被授权的该工单,不能因此获得团队其他任务的权限。成员停用、移出组织或团队,或撤销 TASK_HANDLER 时,必须锁定所有受影响的 active 外部任务并在同一事务处理:当前负责人先原子改派给同一白名单或有效任务级例外中的其他 active 处理方,同时移除失效用户的参与人授权;若没有可改派的处理方,拒绝本次成员变更/撤销操作,不能留下负责人无效或处理方为空的任务。读取时仍需实时校验成员状态和任务级例外有效期兜底。外部提交人不能读取或修改该白名单,管理员通过受限元数据视图管理它。

TASK_ACCESS_GRANT(关系驱动的任务级授权) ​
字段类型说明
idPK授权记录 ID
task_idFK → TASK获得可见性的任务
user_idFK → USER被授权用户
relation_idFK → TASK_RELATION授权来源的跨团队 parent_child 关系
grant_typeenumcross_team_parent_child
created_atdatetime授权时间
(UQ)(task_id, user_id, relation_id, grant_type)

仅跨团队内部任务的 parent_child 关系生成隐式可见授权:在同一事务中按关系两端当前的创建人、负责人和显式相关人员生成对侧任务的授权;外部任务不得接收 TASK_ACCESS_GRANT。cross_type 关系不生成通用授权,外部工单内容仍只对提交人和配置处理方开放。负责人、创建人或参与人变更时必须锁定相关关系并增量对账授权,新增身份立即补授,移除身份仅在没有其他关系支持时回收;解除关系时同样回收该关系生成且不再被其他关系支持的记录。

TASK_RELATION_REQUEST(双边关联申请) ​
字段类型说明
idPK申请 ID
source_task_idFK → TASK请求建立关系的承载任务
target_task_idFK → TASK待确认的目标任务
relation_typeenumparent_child / cross_type
mount_state_idFK → STATE_NODE, nullableparent_child 请求要挂载的 source 状态节点;cross_type 必须为空
initiator_task_idFK → TASK发起申请的任务一端;必须等于 source_task_id 或 target_task_id
operationenumestablish / replace / revoke;确认事务按操作类型创建、替换或撤销关系
relation_idFK → TASK_RELATION, nullablereplace / revoke 针对的现有关系;establish 时为空
relation_revisionbigint, nullable申请时读取的现有关系 revision;确认 replace / revoke 时必须仍匹配,避免覆盖并发变更
requester_idFK → USER发起人;必须按 operation 对 initiator_task_id 通过对应关系操作权限
statusenumpending / approved / rejected / expired
confirmer_task_idFK → TASK发起端的对侧任务;必须等于 source_task_id 或 target_task_id 且不同于 initiator_task_id
confirmer_idFK → USER, nullable对侧任务负责人/创建人或对侧团队管理员
expires_atdatetimepending 申请的失效时间;超过该时间只能标记为 expired,不能再确认
created_atdatetime申请时间
resolved_atdatetime, nullable确认、拒绝或过期时间

跨团队且请求者无法同时看到两端时,先创建 pending 申请,只向发起端的对侧展示按 getTaskProjection 过滤后的最小元数据;纯内部任务可展示任务 ID、标题、类型和团队,若任一端是外部任务则申请视图只能展示任务 ID、类型、所属团队、状态和关系方向,不得展示标题、描述、表单字段、评论、提交人或处理方信息。申请必须保存发起端、operation、对侧 confirmer_task_id、现有 relation_id / relation_revision(如有)以及 mount_state_id(parent_child)或等价的不可变关系声明标识;对侧确认事务重新校验它仍属于 source 任务绑定版本及当前声明。establish / replace 确认时创建或替换 TASK_RELATION,revoke 确认时撤销现有关系并清理不再适用的授权和待处理事件;所有操作都要重新校验双方当前权限、组织边界和 DAG。申请被拒绝、过期、版本不匹配或任一端权限失效时不得改边,也不能通过申请本身扩大可见性。

TASK_RELATION(任务关联关系,逻辑层主子/跨类型) ​
字段类型说明
idPK关联 ID
source_task_idFK → TASK关系承载方:parent_child 为父任务,cross_type 为等待联动的外部工单
target_task_idFK → TASK关系被关联方:parent_child 为子任务,cross_type 为产生完成事件的内部任务
relation_typeenumparent_child(主子)/ cross_type(跨类型联动)
mount_state_idFK → STATE_NODE, nullable子任务挂载的父任务主流程状态节点
revisionbigint关系建立或更换时递增,作为补发自动流转事件的幂等来源
statusenumactive / replaced / revoked;只有 active 的关系参与联动条件判断
(UQ)(source_task_id, target_task_id, relation_type) WHERE status='active',历史关系不占用当前关系唯一性
(PARTIAL UQ)target_task_id WHERE relation_type='parent_child' AND status='active',每个任务最多一个当前父任务
(PARTIAL UQ)source_task_id WHERE relation_type='cross_type' AND status='active',每个外部工单最多一个当前目标

parent_child 关系中 source_task_id 是父任务、target_task_id 是子任务;cross_type 关系中 source_task_id 固定为外部工单、target_task_id 固定为同步创建的内部任务,且 target 必须属于声明的固定目标团队和目标类型,完成事件由 target 推进 source。两种关系的自动流转都只由承载流转任务(parent_child 的 source、cross_type 的 source)绑定版本的 STATE_TRANSITION.trigger_mode/auto_trigger/guard 决定,关系本身不再保存第二份开关。所有边也必须参与全图环检测,但 cross_type 不参与 root_task_id 计算。跨类型关联数据完全隔离(如工单↔内部任务)。 建立跨团队内部任务的 parent_child 关系时,在同一事务中写入 TASK_ACCESS_GRANT 并维护两端及后代的 root_task_id;外部任务与 cross_type 关系不生成通用隐式授权。解除或更换关系时重新计算授权和整棵子树根节点,并同步增量维护关系两端当前创建人、负责人和参与人的授权。 建立或更换关系时若已完成的 target 任务已经满足 source 任务当前状态的 children_done / related_task_done 条件,必须在同一事务写入或唤醒 AUTO_FLOW_OUTBOX,使用新的关系 revision 生成幂等键,不能只依赖 target 任务完成时的事件。替换 cross_type 目标时,必须在同一事务将旧关系标记为 replaced、创建新的 active 关系;自动流转只读取当前 active 关系,旧目标的完成事件不能再次推进 source。

CHECKPOINT(检查点/里程碑) ​
字段类型说明
idPK检查点 ID
task_idFK → TASK所属任务
namestring名称(如 PRD 评审/技术评审)
participantsjson参与人 ID 列表
conclusiontext, nullable结论/备注
done_atdatetime, nullable完成时间
created_atdatetime创建时间

不强制顺序,结构化记录中间进度(PRD 6.1.3)。

TASK_RESOURCE(任务资源) ​
字段类型说明
idPK资源 ID
task_idFK → TASK所属任务
field_keystring对应 TASK_TYPE_VERSION.form_schema 的字段键;storage=task_resource 时必填,用于恢复字段可见性和归档投影
resource_typeenumrepo / pr / doc / link / ...
ref_idstring资源引用 ID(如 PR 号、归档任务 ID)
ref_urlstring, nullable资源链接
metajson扩展元数据;pr 资源必须记录 repository、PR number、当前是否 active、合并状态和 merge commit SHA,供 all_linked_prs_merged guard 聚合判断及已合并事件补发使用

仓库降级为资源(PRD 6.1.1)。PR 资源与 GitHub Webhook 触发的自动流转属于后续迭代(PRD 6.1.5、8.2);归档任务仅作资源引用(PRD 6.1.7)。 新增、替换或刷新 pr 资源时,必须在同一事务按资源的 active/合并状态重新评估当前任务的 pr_merged 自动出边;若 PR 已合并且满足 all_linked_prs_merged,立即写入或唤醒 AUTO_FLOW_OUTBOX。Webhook delivery ID 未知时,以 github:pr:<repository>:<number>:<merge_sha> 作为稳定 source_event_id,不得因 PR 早于资源绑定而漏掉事件。

TASK_COMMENT(任务评论) ​
字段类型说明
idPK评论 ID
task_idFK → TASK所属任务
author_idFK → USER评论作者
parent_idFK → TASK_COMMENT, nullable回复的父评论;必须与当前评论属于同一 task_id,使用 (parent_id, task_id) 复合约束或同一事务领域校验
visibilityenuminternal / shared;外部提交人只能读取 shared
bodytext评论正文
created_atdatetime创建时间
updated_atdatetime更新时间

评论读取必须复用任务可见性和外部任务受限投影;普通内部任务的 internal 评论只对有权内部处理方和管理员可见,shared 评论才可返回给外部提交人。外部任务的受限管理投影覆盖前述通用规则:团队/组织管理员即使可以管理任务,也只能读取评论元数据,不能读取任何评论正文;完整正文只向提交人和处理方白名单返回。 任务归档时评论正文和评论提及与任务一起迁移到 TASK_COMMENT_ARCHIVE / TASK_COMMENT_MENTION_ARCHIVE;归档详情仍复用同一套可见性和字段投影,外部管理员不能通过归档表读取正文。 回复的 parent_id 不得指向其他任务或其他组织的评论;创建/编辑评论时必须锁定父评论并校验 parent_id.task_id = task_id,校验失败写入 SYSTEM_AUDIT,不产生跨任务线程。

TASK_COMMENT_MENTION(评论提及) ​
字段类型说明
comment_idFK → TASK_COMMENT评论
user_idFK → USER被提及用户
(PK)(comment_id, user_id)

创建或编辑评论时,除校验被提及用户对任务有可见性外,还必须按评论本身的 visibility 调用 canViewComment / getCommentProjection 校验收件人能读取评论正文;外部工单的 internal 评论不得提及仅能查看任务元数据的提交人或管理员,也不得为其生成通知。只有正文投影可读时才写入对应的提及通知事件。

NOTIFICATION_PREFERENCE(通知偏好) ​
字段类型说明
user_idFK → USER用户
event_typestring任务分配、状态流转、评论、提及等事件类型
channelenumin_app / im
enabledbool是否启用
quiet_untildatetime, nullable免打扰截止时间
(PK)(user_id, event_type, channel)

没有偏好记录时按“两个渠道默认开启”处理;显式关闭后,该事件不再向对应渠道投递。

NOTIFICATION(站内通知) ​
字段类型说明
idPK通知 ID
user_idFK → USER收件人
task_idFK → TASK, nullable任务来源
event_typestring事件类型
payloadjson展示所需的最小脱敏数据
read_atdatetime, nullable阅读时间
created_atdatetime创建时间
NOTIFICATION_OUTBOX(通知投递箱) ​
字段类型说明
idPK投递记录 ID
notification_idFK → NOTIFICATION通知
channelenumin_app / im
statusenumpending / sent / failed
attemptsint已尝试次数
next_attempt_atdatetime, nullable下次重试时间
(UQ)(notification_id, channel)

状态流转、评论和提及与审计写入同一事务;事务提交后由 outbox worker 按偏好投递,IM 失败只重试该渠道,不影响站内通知或任务状态。

AUDIT_LOG(审计日志/操作留痕) ​
字段类型说明
idPK日志 ID
task_idFK → TASK任务
operator_idFK → USER操作人
actionenumcreate / state_change / field_update / relation_change / approve / close / reopen / archive / comment_create / comment_update / comment_mention
diffjson字段级 diff:[{"field": "assignee_id", "before": "u1", "after": "u2"}, ...];大 JSON 存变更 key 路径及对应 before/after,敏感值只以应用层信封加密形式保存
created_atdatetime操作时间

企业级必备,所有变更可追溯(PRD 7.3)。评论创建、正文编辑和提及增删分别使用 comment_create、comment_update、comment_mention;diff 记录评论/提及的字段路径及 before/after,评论正文仍按敏感字段规则加密和投影,不把原文写入普通审计读取结果。 不存完整快照,仅存变更字段的新旧值(diff);大 JSON 也必须保留变更路径对应的新旧值。敏感字段的 before/after 使用密文和密钥版本保存,getAuditProjection 对普通调用方返回 [REDACTED],仅受控的合规回放服务可解密,因此既满足隐私保护,也支持受控地逆序回放历史状态。 跟随任务归档:任务迁离线表时,关联 AUDIT_LOG 一并迁移。

SYSTEM_AUDIT(系统级审计) ​
字段类型说明
idPK审计记录 ID
org_idFK → ORGANIZATION, nullable已能解析组织时记录;请求尚未确定组织时为空
operator_idFK → USER, nullable操作人;未认证请求或外部身份解析失败时为空
request_idstring请求/追踪 ID
actionenumtask_create_validation / permission_denied / relation_validation / system_error
resource_typestring目标资源类型,如 task / task_relation
resource_idstring, nullable已生成的候选资源 ID;任务尚未创建时允许为空
resultenumrejected / failed
reasonstring脱敏后的失败原因
metadatajson, nullable仅保留最小非敏感上下文
created_atdatetime审计时间

任务创建前的参数、一致性或权限校验失败不能写入 AUDIT_LOG:业务事务必须回滚且不能留下半条 TASK。此类无任务实体的失败统一写入 SYSTEM_AUDIT,并通过独立的 REQUIRES_NEW 事务或可靠审计 outbox 持久化,不能与被回滚的业务事务共用提交边界;任务已存在后的状态、字段和关联变更才写入 AUDIT_LOG。

AUTO_FLOW_OUTBOX(自动流转发布箱) ​
字段类型说明
idempotency_keyPK稳定复合幂等键:内部完成事件使用 task:<source_task_id>:<source_revision>:<flow_task_id>:<transition_id>:<trigger> 或 relation:<source_relation_id>:<source_revision>:<flow_task_id>:<transition_id>:<trigger>;GitHub PR 合并使用 github:<delivery_id>:<flow_task_id>:<transition_id>:pr_merged,Webhook 不可追溯时使用 github:pr:<repository>:<number>:<merge_sha>:<flow_task_id>:<transition_id>:pr_merged。同一 PR 关联多个任务时必须包含 flow_task_id
source_event_idstring, nullable外部事件唯一标识;pr_merged 必填,优先取 GitHub X-GitHub-Delivery 并加 github: 命名空间,资源补发场景取 github:pr:<repository>:<number>:<merge_sha>;内部完成事件为空。payload 另保存 repository 与 PR number 便于追踪
source_task_idFK → TASK, nullable产生完成事件的任务;parent_child 为完成的子任务,cross_type 为完成的内部任务;PR Webhook 场景为空
source_relation_idFK → TASK_RELATION, nullable由关系建立/更换触发的补发事件来源;与 source_event_id、source_task_id 至少有一项非空
source_revisionstring, nullable内部完成任务的状态版本或关系 revision,参与生成稳定幂等键;PR Webhook 使用 source_event_id,不要求该字段
flow_task_idFK → TASK待触发自动流转、由其状态机决定出边的任务(parent_child 场景为父任务)
transition_idFK → STATE_TRANSITION已解析的具体自动出边;消费者不得重新猜测目标节点
triggerenumpr_merged / children_done / related_task_done
payloadjson事件所需的最小元数据,不含敏感正文
statusenumpending / published / failed
attemptsint已发布尝试次数
next_attempt_atdatetime, nullable下次补偿时间
created_atdatetime写入时间
published_atdatetime, nullableMQ 确认发布时间

产生或补发自动流转事件的任务/关系事务必须与 AUTO_FLOW_OUTBOX 同库同事务提交;source_event_id、source_task_id、source_relation_id 至少有一项非空,且 trigger=pr_merged 时必须有 source_event_id。在 parent_child 场景,source_task_id 是完成的子任务,flow_task_id 是承载自动出边的父任务;在 cross_type 场景,source_task_id 是完成的内部任务,flow_task_id 是外部工单。GitHub Webhook 以 X-GitHub-Delivery 作为稳定 delivery ID,并将其纳入 outbox/inbox 的幂等键;重复投递只能命中同一事件。事务提交后 outbox worker 才发布 MQ,发布失败按退避策略重试;同一 delivery 关联多个任务时由 flow_task_id 区分。这样即使提交后进程宕机,事件也不会丢失;发布成功但确认丢失时允许重复投递,由消费侧幂等。

完成事件时点:任务完成定义为其 current_state_id 进入 STATE_NODE.exit_mode=terminal 的状态变更提交成功;该终态变更事务立即为父任务或关联工单写入 AUTO_FLOW_OUTBOX。之后的 status=closed 与 closed_at 只是生命周期关闭和归档计时,不是依赖任务自动流转的触发前置条件。

AUTO_FLOW_EVENT(自动流转幂等事件) ​
字段类型说明
idempotency_keyPK对应发布箱的稳定幂等键,重复投递只能命中一行
source_event_idstring, nullable与 AUTO_FLOW_OUTBOX.source_event_id 一致;GitHub pr_merged 事件保存 github:<delivery_id>,用于追踪和幂等核对
flow_task_idFK → TASK待处理自动流转的任务
triggerenumpr_merged / children_done / related_task_done
transition_idFK → STATE_TRANSITION本次事件对应的具体出边
statusenumpending / processed / ignored
attemptsint已消费尝试次数
next_attempt_atdatetime, nullable条件尚未满足时的下一次评估时间
last_errorstring, nullable最近一次延迟原因(不写入敏感正文)
processed_atdatetime, nullable事务提交成功后的处理时间

这是消费侧 inbox,不承担发布可靠性。消费者在同一事务中先插入或锁定唯一事件,再锁定 flow_task_id 并检查其当前状态、transition_id 对应的出边和触发条件;如果当前状态还不是该 transition 的 from_node_id,不能仅因状态不匹配就标记 ignored,因为完成事件可能早于承载任务进入该节点,必须保留 pending 并由重试扫描器再次评估。承载任务进入对应 from_node_id 或关联条件变化时要在同一事务唤醒该事件;若任务之后重新进入该节点,仍应重新评估尚未失效的 pending 事件。只有关联被 replaced / revoked、transition 已失效、任务已关闭/归档,或已明确确认事件不再适用时,才可标记 ignored。任务已经通过人工或另一事件推进时,也必须先确认该事件确实不再适用;admin_recovery 成功时要在同一事务关闭对应的 pending 事件,避免旧事件再次推进。AUTO_FLOW_OUTBOX.failed 是发布侧状态,只由 outbox worker 重试或结束,不映射为 inbox 状态;消费者自身的可重试错误保留 pending 并更新 last_error / next_attempt_at,不可恢复且已确认失效的事件才标记 ignored。重复事件若已 processed / ignored 可直接 ACK;只有事务提交成功才确认 MQ 消息。

3. 关键设计说明 ​

  • root_task_id 冗余:任务表冗余顶层任务 ID,"仅顶层任务"筛选 = root_task_id = id,避免 DAG 递归查询(PRD 6.2.1、7.6)。由于 parent_child 关系采用单父约束,root_task_id 能表达完整的主子森林;cross_type 关系不改变它。
  • 主子任务逻辑推导:无独立主/子表,TASK_RELATION 的 parent_child 关系推导主子,支持多级嵌套(PRD 6.1.6)。建立、移除或更换父关系时锁定关系图,在同一事务中把目标及其整棵子树的 root_task_id 更新为新根。
  • 关系授权与事件重评估:跨团队关系的隐式授权始终按两端当前成员增量对账;建立/更换关系时若完成条件已经满足,在同一事务写入或唤醒带关系 revision 的自动流转事件。
  • 状态机版本:TASK.type_version_id、current_state_id 和 reopen_state_id 必须属于同一不可变 TASK_TYPE_VERSION;类型修改只创建新版本,历史任务继续使用原版本。
  • 可见性四层判定:组织(org_id 隔离)→ 团队(team_id;cross_team 类型按 visible_team_ids 扩展到同组织允许团队)→ 类型版本(TASK_TYPE_VERSION.visible_roles 与 USER_ROLE,其中 active USER_TEAM 自动满足保留角色 member)→ 单任务授权。单任务授权的来源包括 TASK_PARTICIPANT、当前任务的 created_by / submitter_id、负责人、当前或已配置人工卡点的有效 approver_id,以及跨团队 parent_child 关系驱动的 TASK_ACCESS_GRANT;审批人获得的授权只覆盖对应任务和审批操作,不扩展团队或类型可见性;cross_type 对侧任务不继承外部提交人的任何授权。
  • 管理员权限先于类型矩阵判定:org_admin 在本组织内、team_admin 在所属团队内可执行管理操作,无需拥有 USER_ROLE;但外部任务仍只返回受限管理投影,submitter_only 卡点始终拒绝管理员代审。
  • 字段级投影:PermissionService 除 canViewTask、canPerformAction、canApproveGate 外,必须通过 getTaskProjection / getAuditProjection 按 form_schema.visibility 返回内容;管理员永远不能通过审计日志或通知绕过外部任务脱敏规则。
  • 归档:status=archived 的任务关闭满 2 个月后,与参与人、评论及提及、关联关系、检查点、资源、审计日志一起迁移到对应离线表,并保留只读在线存根(PRD 6.1.7)。
  • 自动流转异步:MVP 的 children_done / related_task_done 流转以及后续的 pr_merged 流转,均由终态变更事务或 Webhook 接收事务写入 AUTO_FLOW_OUTBOX,提交后经 outbox worker 发布 MQ;消费者以 AUTO_FLOW_EVENT 的持久化幂等键和任务条件更新保证一次生效,不等待 status=closed。

4. 查询策略(避免读扩散 & 分库分表友好) ​

架构原则:不依赖 SQL JOIN,应用层组装,保证后期分库分表无需改查询逻辑。

场景策略说明
列表/看板页查 TASK 主表 + 分步批量查 STATE_NODE 补状态名一次请求内分两步:① 查 TASK 列表 ② 收集 current_state_id 集合,批量查 STATE_NODE 取状态名,应用层组装。不依赖内存缓存,避免内存占用过高
列表页子任务不展示列表页不查 TASK_RELATION,不冗余子任务计数
任务详情页只返回 TASK 主表数据子任务/检查点/资源/相关人员点击对应行动点再按需查询,减少单次请求负载
我的视图按 TASK.assignee_id、TASK.created_by、外部 TASK.submitter_id 或 TASK_PARTICIPANT.user_id 筛选各列分别命中索引后在应用层合并去重;不要求把隐式授权主体写入参与人表
审计日志字段级 diff,不存完整快照普通字段保留变更路径和新旧值;敏感值密文保存、投影时脱敏;受控合规服务可逆序回放;跟随任务归档

4.1 为什么不用 JOIN / 不冗余状态名 ​

  • 分库分表:MVP 单库;后续如需分片只按 org_id 分片,保证同一组织内跨团队关系、授权、自动流转事件和归档迁移可以共置并在单库事务中完成。应用层组装解决查询侧的 JOIN 依赖,但不能替代跨分片写入的一致性,因此不得直接按 team_id 分片。
  • 不冗余状态名:STATE_NODE 是低频变更的配置表,列表页通过一次请求内分步批量查询(收集 current_state_id 集合 → 批量查 STATE_NODE)获取状态名,无需冗余到 TASK 表,也无需内存缓存(避免内存占用过高)。
  • 按需查询:详情页关联数据(子任务/检查点/资源)用户不一定每次都看,点击行动点再查更稳妥。

5. 归档表与在线存根 ​

关闭满 2 个月后,归档任务及其依赖数据在同一个数据库事务中成组迁移,避免只迁移任务主体而留下悬空关联:

  • TASK 的完整记录复制到 TASK_ARCHIVE;原在线 TASK 行随后收缩为轻量 status=archived 存根,只保留 id、组织/团队/类型标识、type_version_id、root_task_id、closed_at、archived_at 和 archive_ref 等关联与路由字段。描述、form_data、评论正文等完整内容和对应索引只保留在离线表,在线查询详情必须按需读取归档表,不能继续保留一份完整任务副本。
  • TASK_PARTICIPANT、TASK_HANDLER、CHECKPOINT、TASK_RESOURCE、TASK_COMMENT、TASK_COMMENT_MENTION、AUDIT_LOG 分别复制到对应的 *_ARCHIVE 表,迁移成功后删除在线依赖记录;评论回复的 parent_id 在归档表中保持逻辑引用,TASK_HANDLER_ARCHIVE 继续作为外部工单完整内容的 ACL,归档详情按需从离线表组装。
  • TASK_RELATION 与其 TASK_ACCESS_GRANT 只有在关系两端都已归档时才复制到对应的 *_ARCHIVE 表并删除在线边;若另一端仍活跃,则保留在线关系指向只读 TASK 存根,待两端都归档后再迁移,保证 children_done、隐式授权和关联展示不中断。
  • 归档或删除在线 TASK_RELATION 前,必须锁定所有引用它的 AUTO_FLOW_OUTBOX / AUTO_FLOW_EVENT;先完成可重试发布并等待 inbox 事件变为 processed / ignored,或在关系已失效时将事件明确标记为 ignored。不存在 pending、可重试 failed 或 MQ 在途消息后,才把对应记录按原 idempotency_key 迁移到 AUTO_FLOW_OUTBOX_ARCHIVE / AUTO_FLOW_EVENT_ARCHIVE,再迁移关系;禁止级联删除发布箱记录。处理期间关系保留为只读在线存根,确保不会丢失自动流转。
  • 归档表中的 task_id 使用归档任务 ID 的逻辑引用,不依赖在线表外键;读取归档详情时按需查询对应归档表并在应用层组装。
  • 归档任务不可再建立任何关联,只有 TASK_RESOURCE 的 ref_id / ref_url 可以把它作为只读资源引用。迁移失败则整笔事务回滚,在线数据保持不变。

2. MVP 任务类型状态机 ​

以下三份固定配置均作为对应 TASK_TYPE_VERSION v1 发布;后续调整通过创建新版本完成,不修改本节既有版本。

1. 需求(内部) ​

  • 可见范围:internal
  • 可见角色:全部内部角色
  • 核心字段与表单字段:due_at(期望完成日期)写入 TASK 规范化字段;PRD 文档链接、设计稿链接写入 TASK_RESOURCE,优先级写入 form_data
  • 节点实例参数:PRD评审中 和 技术评审中 复用 human_gate_approver schema,approver_id 必填;审批人必须是同组织内 active 的内部成员。创建任务时缺少任一审批人配置必须拒绝创建,不能把任务放入无审批人的人工卡点。

状态机 ​

节点明细 ​

状态名exit_modeauto_trigger说明
待评审manual—初始状态,手动发起评审
PRD评审中human_gate—gate_policy=configured_approver,审批人由任务实例指定
技术评审中human_gate—gate_policy=configured_approver,审批人由任务实例指定
开发中autochildren_done至少有一个子任务且全部完成时自动流转;没有子任务时改走手动确认边
测试中manual—手动关闭
已完成terminal—终态,不允许继续流转

该类型的唯一初始节点是 待评审(is_initial=true),其余节点均为 false。

流转规则 ​

前置后置触发方式自动触发器操作者策略guard出边标签说明
待评审PRD评审中manual—defaultnone—手动发起 PRD 评审
PRD评审中技术评审中human_gate—defaultnone通过人工卡点放行
PRD评审中待评审human_gate—defaultnone驳回驳回回退
技术评审中开发中human_gate—defaultnone通过人工卡点放行
技术评审中待评审human_gate—defaultnone驳回驳回回退
开发中测试中autochildren_donedefaultchildren_done—至少一个子任务全部完成后自动触发
开发中测试中manual—defaultno_children无子任务时手动确认没有子任务时由负责人明确确认进入测试
测试中已完成manual—defaultnone—手动关闭

关联声明 ​

  • 可挂载子任务(开发任务)到"开发中"状态节点。

2. 开发任务(内部) ​

  • 可见范围:internal
  • 可见角色:全部内部角色
  • 字段映射:仓库、PR 链接写入 TASK_RESOURCE;分支、预估工时写入 form_data

状态机 ​

节点明细 ​

状态名exit_modeauto_trigger说明
待开发manual—初始状态
开发中manual—开发进行中
待合并manual(MVP)—MVP 手动完成;后续启用 pr_merged 时改由 GitHub Webhook 接收一个或多个 PR 的合并事件,全部 active PR 合并后自动流转
已完成terminal—终态,不允许继续流转

该类型的唯一初始节点是 待开发(is_initial=true),其余节点均为 false。

流转规则 ​

前置后置触发方式自动触发器操作者策略guard出边标签说明
待开发开发中manual—defaultnone—手动开始开发
开发中待合并manual—defaultnone—手动标记已提 PR
待合并已完成manual(MVP)—defaultnone—MVP 手动完成;后续版本可替换为 pr_merged + all_linked_prs_merged,由 Webhook 自动触发
待合并开发中manual—defaultnone打回手动打回继续开发

关联声明 ​

  • 可作为"需求"类型任务的子任务,挂载到需求的"开发中"状态节点。
  • MVP 仅记录普通资源;后续迭代增加 PR 资源后,由 GitHub Webhook 校验签名并匹配任务资源,经 AUTO_FLOW_OUTBOX → MQ 触发自动流转。

3. 外部工单(外部) ​

  • 可见范围:external
  • 可见角色:提交人 + 被授权处理方(团队管理员,配置用户 ID 列表)
  • 团队路由:固定配置 external_default_team_id,外部提交入口先按该稳定路由确定团队,再选择该团队启用的版本,不接受外部用户传入任意团队;MVP 每个外部工单类型使用一个默认处理团队。
  • 表单字段:客户联系方式、反馈截图、问题描述、优先级
  • 领取规则:任务初始处于“待处理”时 assignee_id 可为空;处理方从 TASK_HANDLER 白名单中领取,服务端以行锁或乐观锁原子写入负责人后才能进入“处理中”。

以下状态机是后续外部任务接入的契约示例;MVP 只保存该配置,不开放外部登录、提交和确认入口。

状态机 ​

节点明细 ​

状态名exit_modeauto_trigger说明
待处理manual—初始状态,等待内部处理方领取
处理中autorelated_task_done关联的内部任务完成后自动进入确认;管理员不能用普通流转绕过该事件
待用户确认human_gate—gate_policy=submitter_only,仅工单提交人可确认或驳回
已完成terminal—终态,不允许继续流转

该类型的唯一初始节点是 待处理(is_initial=true),其余节点均为 false。

流转规则 ​

前置后置触发方式自动触发器操作者策略guard出边标签说明
待处理处理中manual—defaultnone—处理方手动开始处理
处理中待用户确认autorelated_task_donedefaultrelated_task_done—关联内部任务完成,通知提交人确认
处理中待用户确认manual—admin_recoveryadmin_recovery_pending管理补偿关联任务已完成但事件丢失时,团队/组织管理员可重送确认;不得直接完成或关闭工单
待用户确认已完成human_gate—defaultnone确认人工卡点放行,仅提交人可操作
待用户确认处理中human_gate—defaultnone驳回提交人驳回,补充信息后重新处理

关联声明 ​

  • 创建工单时按任务类型版本 relation_declarations 中预先绑定的 target_task_type_id 同步创建内部任务,二者通过 cross_type 关联;MVP 不从外部提交数据推断或让外部提交人选择内部任务类型。服务端按目标任务所属团队的 active/default 版本解析其 type_version_id,外部工单的 submitter_id 保留在工单上,同步创建的内部任务使用平台 service 账号作为 created_by 且 submitter_id=NULL。
  • cross_type 的目标团队同样由 relation_declarations.target_team_id 固定配置;创建事务先校验该团队与外部工单属于同一组织,再在该团队范围内解析目标类型的 TASK_TYPE_ACTIVE_VERSION,无团队覆盖时回退组织级默认版本。外部提交人和处理方都不能改写这个路由。
  • 内部任务完成后,工单自动流转到"待用户确认"状态(联动规则)。
  • 提交人驳回后工单回到“处理中”,平台向固定 target_team_id 的目标任务负责人/创建人和团队管理员生成 follow-up 待办;只有这些目标团队内部执行者可以重新打开原内部任务或创建 follow-up 内部任务,外部工单处理方不因 cross_type 关系获得内部任务可见性或操作权。目标团队执行者完成操作后更新/新建 cross_type 关系;该内部任务再次完成时以新的任务版本生成新的 related_task_done outbox 事件,工单再次进入“待用户确认”。若目标任务没有负责人或创建人可操作,则由目标团队管理员处理;不得复用已消费的旧幂等键。
  • 数据完全隔离:工单与内部任务各自可见性独立。

4. 配置对比总览 ​

维度需求开发任务外部工单
可见范围internalinternalexternal
人工卡点数2(PRD评审、技术评审)01(待用户确认)
auto 节点开发中(children_done)无(MVP 手动;后续 pr_merged)处理中(related_task_done,后续外部接入)
子任务挂载开发中 ← 开发任务无无
跨类型关联无作为需求的子任务同步创建内部任务
终态已完成已完成已完成

3. 核心流程 ​

0. 全流程总览 ​

四个阶段:

  1. 创建任务类型(管理员,组织级,低频)
  2. 创建任务(用户,按类型表单渲染)
  3. 推进任务(按出边的 trigger_mode、operator_policy 与 guard 流转)
  4. 任务完成(关闭 → gap 期 → 归档)

1. 创建任务类型(管理员) ​

组织级配置,一次配置全组织复用。MVP 阶段写死 3 份(需求/开发任务/外部工单),不实现可视化配置界面。

关键产物(写入 TASK_TYPE + TASK_TYPE_VERSION + STATE_NODE + STATE_TRANSITION):

  • 可见范围 + 可见角色列表
  • 状态机:节点(含 exit_mode / auto_trigger)+ 流转规则
  • TASK_TYPE_VERSION:可见范围、角色矩阵、外部路由、表单字段、节点实例参数表单和关联声明的不可变版本
  • 关联关系声明

2. 创建任务(用户) ​

表单由任务类型配置动态渲染,用户填写后落库。

关键产物(写入 TASK + TASK_PARTICIPANT + TASK_RESOURCE + TASK_RELATION):

  • TASK:基本信息 + current_state_id(初始节点)+ node_overrides + form_data + root_task_id + created_by;外部工单额外写入 submitter_id,内部同步任务使用 service 创建者且不写入外部提交人
  • TASK_PARTICIPANT:相关人员(触发单任务可见性授权)
  • TASK_RELATION:跨类型关联(如外部工单同步创建内部任务)
  • TASK_RESOURCE:按任务类型 schema 将仓库、文档等资源单独落表,不塞入 form_data。
  • TASK_ACCESS_GRANT:跨团队 parent_child 关系产生的隐式任务级授权。
  • created_by:作为当前任务的创建人产生隐式授权,不因负责人被改派而失去任务访问权;外部工单另以 submitter_id 作为提交人授权主体,且该授权不传播到 cross_type 对侧任务。

3. 推进任务(状态流转) ​

任务创建后处于初始节点,运行时按当前节点可用出边的 trigger_mode、operator_policy 与 guard 决定如何流转到下一节点;节点 exit_mode 只作为配置默认值,不能阻断同一节点上已声明且 guard 满足的手动例外边。terminal 节点不允许出边,其余节点按出边触发方式进入对应的推进路径。

同一节点可以同时声明自动边和受限手动例外边:例如“开发中”节点没有子任务时选择 no_children 手动边,自动事件仍按 children_done 边处理;“待合并”的打回边和外部工单的 admin_recovery 边也必须按各自的 trigger_mode / guard 提供可达入口。只有 guard 不满足时才拒绝该出边,不能用节点默认 exit_mode=auto 隐藏手动例外。

无论是 manual、human_gate 还是 auto,执行 UPD 都必须在同一事务中锁定任务,或使用 version + current_state_id 做条件更新;只有一个请求能成功推进,成功后的状态变更、AUDIT_LOG 和通知 outbox 一起提交,过期请求返回冲突并不产生审计或通知。每条边的 guard 也必须在同一锁或条件更新中校验,未满足时返回冲突,不得仅依赖界面标签。触发父任务或关联任务自动流转的完成事务还必须在同一事务写入 AUTO_FLOW_OUTBOX;outbox worker 只在提交后发布 MQ,发布失败按退避重试。

3.1 节点离开方式与出边触发方式 ​

节点默认 exit_mode常见出边 trigger_mode触发方触发条件是否经 MQ典型场景
manualmanual用户用户手动选择流转否(同步)待开发→开发中
autoauto(可声明受限手动例外)系统auto_trigger 条件满足(子任务/关联任务完成;PR 合并属于后续集成)是(异步幂等)开发中→测试中(children_done,MVP)
human_gatehuman_gate审批人按 gate_policy 确认/驳回否(同步)待用户确认→已完成
terminal无—已到终态,不允许继续流转否已完成

exit_mode 是节点默认值和能力声明,运行时以 STATE_TRANSITION.trigger_mode、operator_policy 与 guard 为准;terminal 节点不得作为任何出边的前置节点。

3.2 自动流转(auto)异步链路 ​

以下以后续迭代的 GitHub PR 合并 Webhook 为例;MVP 的内部完成事件跳过 Webhook 接收步骤,但沿用同一套 outbox、MQ 和幂等消费链路。

发布箱保证事件不丢失,消费者 inbox 保证事件幂等(PRD 7.6):同一事件重复发布或消费不重复流转。

3.3 人工卡点(human_gate)审批 ​

  • 审批人从 TASK.node_overrides[node_id].approver_id 读取(任务实例级);“需求”类型的两个人工卡点在创建时必须由 human_gate_approver schema 提供该字段并通过组织成员校验。
  • 普通 human_gate 仅允许审批人 / 团队管理员 / 组织管理员操作;外部工单的 gate_policy=submitter_only 是更严格的例外,仅工单提交人可确认或驳回,管理员不得代审(PRD 4.2.3、6.4)。
  • 通过/驳回分别走对应出边(STATE_TRANSITION.label)。

4. 任务完成(关闭 / 归档) ​

关键规则(PRD 6.1.7):

  • 任务只能关闭/归档,不可删除(保留审计与关联完整性)。
  • status=closed 只能从 STATE_NODE.exit_mode=terminal 的当前节点写入;服务端必须在同一事务校验所有终态前置 guard、人工卡点结果和关闭权限,非终态任务不得直接关闭。外部工单的提交人确认是进入终态的人工卡点,不能与关闭动作混为绕过状态机的管理操作。
  • gap 期内任务仍在在线表,重新打开将 status 改回 active,同时把 current_state_id 恢复为 reopen_state_id,并清空 closed_at。
  • 超期迁离线表,关联评论、评论提及和 AUDIT_LOG 一并迁移;归档详情继续执行外部任务的受限投影。
  • 归档后不可建立关联,仅可作为任务资源被引用(挂 ID/链接)。
  • 离线表不自动删除/冷存储,仅人工转压缩包(年为单位)。

5. 跨阶段数据流总览 ​

阶段主要写入表主要读取表
创建任务类型TASK_TYPE / TASK_TYPE_VERSION / STATE_NODE / STATE_TRANSITION—
创建任务TASK / TASK_PARTICIPANT / TASK_RELATIONTASK_TYPE.default_version_id / TASK_TYPE_ACTIVE_VERSION / TASK_TYPE_VERSION / STATE_NODE(渲染表单)
推进任务TASK(current_state_id)/ AUDIT_LOGTASK.type_version_id / STATE_NODE / STATE_TRANSITION / TASK_RELATION(auto 校验)
任务完成TASK(status/archived_at)/ 各类 *_ARCHIVETASK / 各类 *_ARCHIVE(按需)