外观
任务管理平台技术方案
对应 PRD 原文。本文定义数据模型、MVP 状态机和核心流程;两者冲突时以 PRD 为准并回头修本文。 需求来源:lamolabs/lamolabs-docs#48。
1. 数据模型与查询策略
1. ER 总览
2. 实体字段明细
2.1 组织与用户
ORGANIZATION(组织)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 组织 ID |
| name | string | 组织名称 |
| created_at | datetime | 创建时间 |
最高隔离边界,跨组织严禁访问(PRD 4.2.1)。
TEAM(团队)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 团队 ID |
| org_id | FK → ORGANIZATION | 所属组织 |
| name | string | 团队名称 |
| created_at | datetime | 创建时间 |
组织下协作单元,跨团队任务默认隔离(PRD 4.2.1)。
ITERATION(迭代排期)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 迭代 ID |
| org_id | FK → ORGANIZATION | 所属组织 |
| team_id | FK → TEAM | 所属团队 |
| name | string | 迭代名称 |
| starts_at | datetime, nullable | 开始时间 |
| ends_at | datetime, nullable | 结束时间 |
| created_at | datetime | 创建时间 |
迭代属于团队,任务通过
iteration_id关联;同一组织跨团队的排期仍分别维护,避免把不同团队的迭代混为一谈。
USER_ORG_MEMBERSHIP(用户-组织归属)
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | FK → USER | 用户 |
| org_id | FK → ORGANIZATION | 组织 |
| membership_type | enum | member / external_guest |
| status | enum | active / suspended |
| joined_at | datetime | 加入时间 |
| (PK) | (user_id, org_id) |
所有用户必须先有有效的组织归属;外部访客可以不加入任何团队,但只能访问被授权的外部任务。负责人、创建人、提交人和参与人都必须通过该表校验组织边界。
USER(用户)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 用户 ID(来自认证中心) |
| name | string | 姓名 |
| kind | enum | human / service;平台系统服务账号使用 service |
| created_at | datetime | 创建时间 |
用户是认证中心的全局身份;是否为内部成员或外部访客由
USER_ORG_MEMBERSHIP.membership_type按组织决定,不使用全局is_external标志。与团队多对多,外部访客可不加入团队(PRD 4.2.1、6.4)。kind=service的平台系统服务账号必须以 active 的内部成员归属绑定到目标组织,只能作为后台操作主体,不能作为外部提交人、负责人或任务参与人。
USER_TEAM(用户-团队关系)
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | FK → USER | 用户 |
| team_id | FK → TEAM | 团队 |
| role | enum | 该用户在此团队的角色:member / team_admin |
| joined_at | datetime | 加入时间 |
| (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_id | FK → USER | 用户 |
| org_id | FK → ORGANIZATION | 组织 |
| role | enum | org_admin |
| (PK) | (user_id, org_id) |
组织角色从架构角度划分:成员 / 团队管理员 / 组织管理员(PRD 4.2.3)。该记录必须对应
USER.kind=human、active 且membership_type=member的USER_ORG_MEMBERSHIP;service账号和外部访客不能获得org_admin。"任务负责人"是任务级属性,不在此表。
ROLE(组织级任务角色)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 角色 ID |
| org_id | FK → ORGANIZATION | 所属组织 |
| code | string | 角色编码,如 developer / tester |
| name | string | 角色名称 |
| created_at | datetime | 创建时间 |
这是任务类型可见性矩阵使用的功能角色目录,与
member/team_admin/org_admin组织角色分离。MVP 预置保留角色member以及功能角色developer、tester,后续可由组织管理员扩展。
USER_ROLE(用户-任务角色关系)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 分配记录 ID |
| user_id | FK → USER | 用户 |
| role_id | FK → ROLE | 任务角色 |
| team_id | FK → TEAM | 角色生效的团队 |
| created_at | datetime | 分配时间 |
| (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(任务类型)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 类型 ID |
| org_id | FK → ORGANIZATION | 所属组织(组织级共享) |
| name | string | 类型名(如 需求/开发任务/外部工单) |
| default_version_id | FK → TASK_TYPE_VERSION, nullable | 组织级默认的已发布配置版本;无团队覆盖时使用 |
| external_default_team_id | FK → TEAM, nullable | external 类型的稳定默认处理团队;在选择类型版本前确定路由,外部提交人不能传入 |
| external_handler_user_ids | json, nullable | external 类型的稳定处理方白名单;必须全部属于 external_default_team_id,发布版本时复制为版本快照 |
| created_at | datetime | 创建时间 |
TASK_TYPE保存稳定身份和外部工单的固定路由;可见范围、角色矩阵、字段、状态机和关联声明归档在不可变的TASK_TYPE_VERSION。管理员修改类型时创建并发布新版本,不能原地改写已有版本。external_default_team_id/external_handler_user_ids是外部路由的稳定来源,发布版本时复制为快照并要求一致;因此外部任务可以先确定处理团队,再按该团队选择启用版本。MVP 为需求、开发任务、外部工单各发布组织级v1;每个团队可通过TASK_TYPE_ACTIVE_VERSION选择自己的已发布版本,没有团队覆盖时使用默认版本。
TASK_TYPE_VERSION(任务类型配置版本)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 配置版本 ID |
| type_id | FK → TASK_TYPE | 所属任务类型 |
| team_id | FK → TEAM, nullable | 生效团队;NULL 表示组织级默认版本 |
| version | int | 同一类型、同一生效范围内递增的版本号 |
| visibility_scope | enum | 可见范围:internal / external / cross_team |
| visible_roles | json | 可见角色 ID 列表(引用 ROLE,角色 × 类型可见性矩阵) |
| visible_team_ids | json, nullable | cross_team 类型允许查看的组织内团队 ID 列表;必须包含任务所属团队,其他范围必须为空 |
| external_default_team_id | FK → TEAM, nullable | 从 TASK_TYPE.external_default_team_id 复制的版本快照;external 类型必填且必须与稳定路由一致 |
| handler_user_ids | json, nullable | 外部工单的处理方用户 ID 白名单版本快照,仅管理员可配置/读取;external 版本必须非空且全部属于 external_default_team_id |
| form_schema | json | 资源/表单字段定义(按类型配置);每个字段必须声明 storage=form_data 或 storage=task_resource,并声明 visibility=metadata / shared / internal。仓库、PR、文档、链接等资源型字段只能写入 TASK_RESOURCE,普通自定义字段只能写入 TASK.form_data |
| node_overrides_form_schemas | json | 节点级实例参数的表单 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_declarations | json | 可声明的关联关系及联动规则;cross_type 必须预先写入固定的 target_team_id 与 target_task_type_id(MVP 不从外部提交输入推断目标团队或类型) |
| status | enum | draft / published / retired |
| created_at | datetime | 创建时间 |
| (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_id | FK → TASK_TYPE | 任务类型 |
| team_id | FK → TEAM | 生效团队 |
| version_id | FK → 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(状态节点,任务类型版本的状态机)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 节点 ID |
| type_version_id | FK → TASK_TYPE_VERSION | 所属任务类型配置版本 |
| name | string | 状态名 |
| exit_mode | enum | 默认离开方式:manual(手动推进)/ auto(外部事件触发)/ human_gate(人工卡点)/ terminal(终态) |
| auto_trigger | enum, nullable | 默认自动触发器(默认 exit_mode=auto 时必填):pr_merged / children_done / related_task_done / ... |
| gate_policy | enum, nullable | 人工卡点策略:configured_approver / submitter_only,仅 human_gate 使用 |
| is_initial | bool | 是否为该配置版本的初始节点;每个 TASK_TYPE_VERSION 必须且只能有一个 |
| sort | int | 顺序 |
| (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(状态流转规则)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 规则 ID |
| from_node_id | FK → STATE_NODE | 前置状态 |
| to_node_id | FK → STATE_NODE | 后置状态 |
| label | string, nullable | 出边标签(如 human_gate 的"通过"/"驳回") |
| trigger_mode | enum | 该出边的触发方式:manual / auto / human_gate |
| auto_trigger | enum, nullable | trigger_mode=auto 时的触发器:pr_merged / children_done / related_task_done / ... |
| operator_policy | enum | 操作者策略:default / admin_recovery;管理补偿只能用于预先声明的人工补偿边 |
| guard | enum | 边前置条件: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 映射到保留哨兵值;不支持这两类能力时,由同一事务内的领域校验拒绝重复组合,不能依赖普通 SQLUNIQUE的 NULL 语义。
2.3 任务核心
TASK(任务)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 任务 ID |
| team_id | FK → TEAM | 所属团队 |
| org_id | FK → ORGANIZATION | 所属组织(冗余,便于组织级查询) |
| type_id | FK → TASK_TYPE | 任务类型 |
| type_version_id | FK → TASK_TYPE_VERSION | 不可变的任务类型配置版本;新任务取当前已发布版本,存量任务不随类型编辑漂移 |
| iteration_id | FK → ITERATION, nullable | 所属迭代排期;必须与任务团队属于同一组织/团队 |
| title | string, nullable when archived | 标题 |
| description | text, nullable when archived | 描述 |
| assignee_id | FK → USER, nullable | 负责人(唯一);外部工单处于“待处理”时允许为空,处理方领取时原子写入 |
| current_state_id | FK → STATE_NODE, nullable when archived | 当前主状态;归档存根不再保留 |
| root_task_id | FK → TASK(延迟约束) | 顶层任务 ID(冗余且非空),无父任务时必须 = 自身 id;提升查询性能,避免 DAG 递归(PRD 7.6)。该自引用外键使用 DEFERRABLE INITIALLY DEFERRED,在事务提交时再校验 |
| node_overrides | json, nullable | 任务层面对节点的补充信息(实例级参数):{"node_id": {"approver_id": "user_123"}, ...},key = 节点 ID,value = 该节点在此任务实例下的参数。当前用于 human_gate 审批人,后续可扩展任意节点级实例参数 |
| form_data | json, nullable when archived | 按类型 form_schema 填写的自定义字段值 |
| status | enum | active / closed / archived |
| closed_at | datetime, nullable | 关闭时间(gap 期起算点) |
| reopen_state_id | FK → STATE_NODE, nullable | 关闭时记录的恢复目标,默认取关闭前最后一个非终态节点 |
| archived_at | datetime, nullable | 归档时间(迁离线表) |
| archive_ref | string, nullable | 归档详情记录的稳定引用;仅 status=archived 时填写 |
| version | bigint | 状态与关键字段的乐观锁版本,每次成功更新递增 |
| due_at | datetime, nullable | 规范化截止时间 |
| created_by | FK → USER, nullable when archived | 创建人 |
| submitter_id | FK → USER, nullable | 仅外部任务必填的提交人;用于外部内容可见性和 submitter_only 卡点,不传播到关联内部任务 |
| created_at | datetime, nullable when archived | 创建时间 |
| updated_at | datetime, 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 的内部成员,并存在同团队的 activeUSER_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必须命中该任务的 activeTASK_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_id | FK → TASK | 任务 |
| user_id | FK → 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_id | FK → TASK | 外部工单 |
| user_id | FK → USER | 处理方用户 |
| assigned_by | FK → USER | 指派人 |
| status | enum | active / revoked;仅 active 记录可参与完整内容授权 |
| grant_scope | enum | version_default / task_override;任务级例外仅用于存量任务处理方迁移 |
| expires_at | datetime, nullable | task_override 的失效时间;默认版本授权为空 |
| grant_reason | string, nullable | 任务级例外的审计原因;task_override 时必填 |
| revoked_at | datetime, nullable | 撤销时间 |
| created_at | datetime | 授权时间 |
| (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(关系驱动的任务级授权)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 授权记录 ID |
| task_id | FK → TASK | 获得可见性的任务 |
| user_id | FK → USER | 被授权用户 |
| relation_id | FK → TASK_RELATION | 授权来源的跨团队 parent_child 关系 |
| grant_type | enum | cross_team_parent_child |
| created_at | datetime | 授权时间 |
| (UQ) | (task_id, user_id, relation_id, grant_type) |
仅跨团队内部任务的
parent_child关系生成隐式可见授权:在同一事务中按关系两端当前的创建人、负责人和显式相关人员生成对侧任务的授权;外部任务不得接收TASK_ACCESS_GRANT。cross_type关系不生成通用授权,外部工单内容仍只对提交人和配置处理方开放。负责人、创建人或参与人变更时必须锁定相关关系并增量对账授权,新增身份立即补授,移除身份仅在没有其他关系支持时回收;解除关系时同样回收该关系生成且不再被其他关系支持的记录。
TASK_RELATION_REQUEST(双边关联申请)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 申请 ID |
| source_task_id | FK → TASK | 请求建立关系的承载任务 |
| target_task_id | FK → TASK | 待确认的目标任务 |
| relation_type | enum | parent_child / cross_type |
| mount_state_id | FK → STATE_NODE, nullable | parent_child 请求要挂载的 source 状态节点;cross_type 必须为空 |
| initiator_task_id | FK → TASK | 发起申请的任务一端;必须等于 source_task_id 或 target_task_id |
| operation | enum | establish / replace / revoke;确认事务按操作类型创建、替换或撤销关系 |
| relation_id | FK → TASK_RELATION, nullable | replace / revoke 针对的现有关系;establish 时为空 |
| relation_revision | bigint, nullable | 申请时读取的现有关系 revision;确认 replace / revoke 时必须仍匹配,避免覆盖并发变更 |
| requester_id | FK → USER | 发起人;必须按 operation 对 initiator_task_id 通过对应关系操作权限 |
| status | enum | pending / approved / rejected / expired |
| confirmer_task_id | FK → TASK | 发起端的对侧任务;必须等于 source_task_id 或 target_task_id 且不同于 initiator_task_id |
| confirmer_id | FK → USER, nullable | 对侧任务负责人/创建人或对侧团队管理员 |
| expires_at | datetime | pending 申请的失效时间;超过该时间只能标记为 expired,不能再确认 |
| created_at | datetime | 申请时间 |
| resolved_at | datetime, 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(任务关联关系,逻辑层主子/跨类型)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 关联 ID |
| source_task_id | FK → TASK | 关系承载方:parent_child 为父任务,cross_type 为等待联动的外部工单 |
| target_task_id | FK → TASK | 关系被关联方:parent_child 为子任务,cross_type 为产生完成事件的内部任务 |
| relation_type | enum | parent_child(主子)/ cross_type(跨类型联动) |
| mount_state_id | FK → STATE_NODE, nullable | 子任务挂载的父任务主流程状态节点 |
| revision | bigint | 关系建立或更换时递增,作为补发自动流转事件的幂等来源 |
| status | enum | active / 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(检查点/里程碑)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 检查点 ID |
| task_id | FK → TASK | 所属任务 |
| name | string | 名称(如 PRD 评审/技术评审) |
| participants | json | 参与人 ID 列表 |
| conclusion | text, nullable | 结论/备注 |
| done_at | datetime, nullable | 完成时间 |
| created_at | datetime | 创建时间 |
不强制顺序,结构化记录中间进度(PRD 6.1.3)。
TASK_RESOURCE(任务资源)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 资源 ID |
| task_id | FK → TASK | 所属任务 |
| field_key | string | 对应 TASK_TYPE_VERSION.form_schema 的字段键;storage=task_resource 时必填,用于恢复字段可见性和归档投影 |
| resource_type | enum | repo / pr / doc / link / ... |
| ref_id | string | 资源引用 ID(如 PR 号、归档任务 ID) |
| ref_url | string, nullable | 资源链接 |
| meta | json | 扩展元数据;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(任务评论)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 评论 ID |
| task_id | FK → TASK | 所属任务 |
| author_id | FK → USER | 评论作者 |
| parent_id | FK → TASK_COMMENT, nullable | 回复的父评论;必须与当前评论属于同一 task_id,使用 (parent_id, task_id) 复合约束或同一事务领域校验 |
| visibility | enum | internal / shared;外部提交人只能读取 shared |
| body | text | 评论正文 |
| created_at | datetime | 创建时间 |
| updated_at | datetime | 更新时间 |
评论读取必须复用任务可见性和外部任务受限投影;普通内部任务的
internal评论只对有权内部处理方和管理员可见,shared评论才可返回给外部提交人。外部任务的受限管理投影覆盖前述通用规则:团队/组织管理员即使可以管理任务,也只能读取评论元数据,不能读取任何评论正文;完整正文只向提交人和处理方白名单返回。 任务归档时评论正文和评论提及与任务一起迁移到TASK_COMMENT_ARCHIVE/TASK_COMMENT_MENTION_ARCHIVE;归档详情仍复用同一套可见性和字段投影,外部管理员不能通过归档表读取正文。 回复的parent_id不得指向其他任务或其他组织的评论;创建/编辑评论时必须锁定父评论并校验parent_id.task_id = task_id,校验失败写入SYSTEM_AUDIT,不产生跨任务线程。
TASK_COMMENT_MENTION(评论提及)
| 字段 | 类型 | 说明 |
|---|---|---|
| comment_id | FK → TASK_COMMENT | 评论 |
| user_id | FK → USER | 被提及用户 |
| (PK) | (comment_id, user_id) |
创建或编辑评论时,除校验被提及用户对任务有可见性外,还必须按评论本身的
visibility调用canViewComment/getCommentProjection校验收件人能读取评论正文;外部工单的internal评论不得提及仅能查看任务元数据的提交人或管理员,也不得为其生成通知。只有正文投影可读时才写入对应的提及通知事件。
NOTIFICATION_PREFERENCE(通知偏好)
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | FK → USER | 用户 |
| event_type | string | 任务分配、状态流转、评论、提及等事件类型 |
| channel | enum | in_app / im |
| enabled | bool | 是否启用 |
| quiet_until | datetime, nullable | 免打扰截止时间 |
| (PK) | (user_id, event_type, channel) |
没有偏好记录时按“两个渠道默认开启”处理;显式关闭后,该事件不再向对应渠道投递。
NOTIFICATION(站内通知)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 通知 ID |
| user_id | FK → USER | 收件人 |
| task_id | FK → TASK, nullable | 任务来源 |
| event_type | string | 事件类型 |
| payload | json | 展示所需的最小脱敏数据 |
| read_at | datetime, nullable | 阅读时间 |
| created_at | datetime | 创建时间 |
NOTIFICATION_OUTBOX(通知投递箱)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 投递记录 ID |
| notification_id | FK → NOTIFICATION | 通知 |
| channel | enum | in_app / im |
| status | enum | pending / sent / failed |
| attempts | int | 已尝试次数 |
| next_attempt_at | datetime, nullable | 下次重试时间 |
| (UQ) | (notification_id, channel) |
状态流转、评论和提及与审计写入同一事务;事务提交后由 outbox worker 按偏好投递,IM 失败只重试该渠道,不影响站内通知或任务状态。
AUDIT_LOG(审计日志/操作留痕)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 日志 ID |
| task_id | FK → TASK | 任务 |
| operator_id | FK → USER | 操作人 |
| action | enum | create / state_change / field_update / relation_change / approve / close / reopen / archive / comment_create / comment_update / comment_mention |
| diff | json | 字段级 diff:[{"field": "assignee_id", "before": "u1", "after": "u2"}, ...];大 JSON 存变更 key 路径及对应 before/after,敏感值只以应用层信封加密形式保存 |
| created_at | datetime | 操作时间 |
企业级必备,所有变更可追溯(PRD 7.3)。评论创建、正文编辑和提及增删分别使用
comment_create、comment_update、comment_mention;diff记录评论/提及的字段路径及 before/after,评论正文仍按敏感字段规则加密和投影,不把原文写入普通审计读取结果。 不存完整快照,仅存变更字段的新旧值(diff);大 JSON 也必须保留变更路径对应的新旧值。敏感字段的 before/after 使用密文和密钥版本保存,getAuditProjection对普通调用方返回[REDACTED],仅受控的合规回放服务可解密,因此既满足隐私保护,也支持受控地逆序回放历史状态。 跟随任务归档:任务迁离线表时,关联 AUDIT_LOG 一并迁移。
SYSTEM_AUDIT(系统级审计)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | PK | 审计记录 ID |
| org_id | FK → ORGANIZATION, nullable | 已能解析组织时记录;请求尚未确定组织时为空 |
| operator_id | FK → USER, nullable | 操作人;未认证请求或外部身份解析失败时为空 |
| request_id | string | 请求/追踪 ID |
| action | enum | task_create_validation / permission_denied / relation_validation / system_error |
| resource_type | string | 目标资源类型,如 task / task_relation |
| resource_id | string, nullable | 已生成的候选资源 ID;任务尚未创建时允许为空 |
| result | enum | rejected / failed |
| reason | string | 脱敏后的失败原因 |
| metadata | json, nullable | 仅保留最小非敏感上下文 |
| created_at | datetime | 审计时间 |
任务创建前的参数、一致性或权限校验失败不能写入
AUDIT_LOG:业务事务必须回滚且不能留下半条 TASK。此类无任务实体的失败统一写入SYSTEM_AUDIT,并通过独立的REQUIRES_NEW事务或可靠审计 outbox 持久化,不能与被回滚的业务事务共用提交边界;任务已存在后的状态、字段和关联变更才写入AUDIT_LOG。
AUTO_FLOW_OUTBOX(自动流转发布箱)
| 字段 | 类型 | 说明 |
|---|---|---|
| idempotency_key | PK | 稳定复合幂等键:内部完成事件使用 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_id | string, nullable | 外部事件唯一标识;pr_merged 必填,优先取 GitHub X-GitHub-Delivery 并加 github: 命名空间,资源补发场景取 github:pr:<repository>:<number>:<merge_sha>;内部完成事件为空。payload 另保存 repository 与 PR number 便于追踪 |
| source_task_id | FK → TASK, nullable | 产生完成事件的任务;parent_child 为完成的子任务,cross_type 为完成的内部任务;PR Webhook 场景为空 |
| source_relation_id | FK → TASK_RELATION, nullable | 由关系建立/更换触发的补发事件来源;与 source_event_id、source_task_id 至少有一项非空 |
| source_revision | string, nullable | 内部完成任务的状态版本或关系 revision,参与生成稳定幂等键;PR Webhook 使用 source_event_id,不要求该字段 |
| flow_task_id | FK → TASK | 待触发自动流转、由其状态机决定出边的任务(parent_child 场景为父任务) |
| transition_id | FK → STATE_TRANSITION | 已解析的具体自动出边;消费者不得重新猜测目标节点 |
| trigger | enum | pr_merged / children_done / related_task_done |
| payload | json | 事件所需的最小元数据,不含敏感正文 |
| status | enum | pending / published / failed |
| attempts | int | 已发布尝试次数 |
| next_attempt_at | datetime, nullable | 下次补偿时间 |
| created_at | datetime | 写入时间 |
| published_at | datetime, nullable | MQ 确认发布时间 |
产生或补发自动流转事件的任务/关系事务必须与
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_key | PK | 对应发布箱的稳定幂等键,重复投递只能命中一行 |
| source_event_id | string, nullable | 与 AUTO_FLOW_OUTBOX.source_event_id 一致;GitHub pr_merged 事件保存 github:<delivery_id>,用于追踪和幂等核对 |
| flow_task_id | FK → TASK | 待处理自动流转的任务 |
| trigger | enum | pr_merged / children_done / related_task_done |
| transition_id | FK → STATE_TRANSITION | 本次事件对应的具体出边 |
| status | enum | pending / processed / ignored |
| attempts | int | 已消费尝试次数 |
| next_attempt_at | datetime, nullable | 条件尚未满足时的下一次评估时间 |
| last_error | string, nullable | 最近一次延迟原因(不写入敏感正文) |
| processed_at | datetime, 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,其中 activeUSER_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_approverschema,approver_id必填;审批人必须是同组织内 active 的内部成员。创建任务时缺少任一审批人配置必须拒绝创建,不能把任务放入无审批人的人工卡点。
状态机
节点明细
| 状态名 | exit_mode | auto_trigger | 说明 |
|---|---|---|---|
| 待评审 | manual | — | 初始状态,手动发起评审 |
| PRD评审中 | human_gate | — | gate_policy=configured_approver,审批人由任务实例指定 |
| 技术评审中 | human_gate | — | gate_policy=configured_approver,审批人由任务实例指定 |
| 开发中 | auto | children_done | 至少有一个子任务且全部完成时自动流转;没有子任务时改走手动确认边 |
| 测试中 | manual | — | 手动关闭 |
| 已完成 | terminal | — | 终态,不允许继续流转 |
该类型的唯一初始节点是 待评审(is_initial=true),其余节点均为 false。
流转规则
| 前置 | 后置 | 触发方式 | 自动触发器 | 操作者策略 | guard | 出边标签 | 说明 |
|---|---|---|---|---|---|---|---|
| 待评审 | PRD评审中 | manual | — | default | none | — | 手动发起 PRD 评审 |
| PRD评审中 | 技术评审中 | human_gate | — | default | none | 通过 | 人工卡点放行 |
| PRD评审中 | 待评审 | human_gate | — | default | none | 驳回 | 驳回回退 |
| 技术评审中 | 开发中 | human_gate | — | default | none | 通过 | 人工卡点放行 |
| 技术评审中 | 待评审 | human_gate | — | default | none | 驳回 | 驳回回退 |
| 开发中 | 测试中 | auto | children_done | default | children_done | — | 至少一个子任务全部完成后自动触发 |
| 开发中 | 测试中 | manual | — | default | no_children | 无子任务时手动确认 | 没有子任务时由负责人明确确认进入测试 |
| 测试中 | 已完成 | manual | — | default | none | — | 手动关闭 |
关联声明
- 可挂载子任务(开发任务)到"开发中"状态节点。
2. 开发任务(内部)
- 可见范围:internal
- 可见角色:全部内部角色
- 字段映射:仓库、PR 链接写入
TASK_RESOURCE;分支、预估工时写入form_data
状态机
节点明细
| 状态名 | exit_mode | auto_trigger | 说明 |
|---|---|---|---|
| 待开发 | manual | — | 初始状态 |
| 开发中 | manual | — | 开发进行中 |
| 待合并 | manual(MVP) | — | MVP 手动完成;后续启用 pr_merged 时改由 GitHub Webhook 接收一个或多个 PR 的合并事件,全部 active PR 合并后自动流转 |
| 已完成 | terminal | — | 终态,不允许继续流转 |
该类型的唯一初始节点是 待开发(is_initial=true),其余节点均为 false。
流转规则
| 前置 | 后置 | 触发方式 | 自动触发器 | 操作者策略 | guard | 出边标签 | 说明 |
|---|---|---|---|---|---|---|---|
| 待开发 | 开发中 | manual | — | default | none | — | 手动开始开发 |
| 开发中 | 待合并 | manual | — | default | none | — | 手动标记已提 PR |
| 待合并 | 已完成 | manual(MVP) | — | default | none | — | MVP 手动完成;后续版本可替换为 pr_merged + all_linked_prs_merged,由 Webhook 自动触发 |
| 待合并 | 开发中 | manual | — | default | none | 打回 | 手动打回继续开发 |
关联声明
- 可作为"需求"类型任务的子任务,挂载到需求的"开发中"状态节点。
- MVP 仅记录普通资源;后续迭代增加 PR 资源后,由 GitHub Webhook 校验签名并匹配任务资源,经
AUTO_FLOW_OUTBOX→ MQ 触发自动流转。
3. 外部工单(外部)
- 可见范围:external
- 可见角色:提交人 + 被授权处理方(团队管理员,配置用户 ID 列表)
- 团队路由:固定配置
external_default_team_id,外部提交入口先按该稳定路由确定团队,再选择该团队启用的版本,不接受外部用户传入任意团队;MVP 每个外部工单类型使用一个默认处理团队。 - 表单字段:客户联系方式、反馈截图、问题描述、优先级
- 领取规则:任务初始处于“待处理”时
assignee_id可为空;处理方从TASK_HANDLER白名单中领取,服务端以行锁或乐观锁原子写入负责人后才能进入“处理中”。
以下状态机是后续外部任务接入的契约示例;MVP 只保存该配置,不开放外部登录、提交和确认入口。
状态机
节点明细
| 状态名 | exit_mode | auto_trigger | 说明 |
|---|---|---|---|
| 待处理 | manual | — | 初始状态,等待内部处理方领取 |
| 处理中 | auto | related_task_done | 关联的内部任务完成后自动进入确认;管理员不能用普通流转绕过该事件 |
| 待用户确认 | human_gate | — | gate_policy=submitter_only,仅工单提交人可确认或驳回 |
| 已完成 | terminal | — | 终态,不允许继续流转 |
该类型的唯一初始节点是 待处理(is_initial=true),其余节点均为 false。
流转规则
| 前置 | 后置 | 触发方式 | 自动触发器 | 操作者策略 | guard | 出边标签 | 说明 |
|---|---|---|---|---|---|---|---|
| 待处理 | 处理中 | manual | — | default | none | — | 处理方手动开始处理 |
| 处理中 | 待用户确认 | auto | related_task_done | default | related_task_done | — | 关联内部任务完成,通知提交人确认 |
| 处理中 | 待用户确认 | manual | — | admin_recovery | admin_recovery_pending | 管理补偿 | 关联任务已完成但事件丢失时,团队/组织管理员可重送确认;不得直接完成或关闭工单 |
| 待用户确认 | 已完成 | human_gate | — | default | none | 确认 | 人工卡点放行,仅提交人可操作 |
| 待用户确认 | 处理中 | human_gate | — | default | none | 驳回 | 提交人驳回,补充信息后重新处理 |
关联声明
- 创建工单时按任务类型版本
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_doneoutbox 事件,工单再次进入“待用户确认”。若目标任务没有负责人或创建人可操作,则由目标团队管理员处理;不得复用已消费的旧幂等键。 - 数据完全隔离:工单与内部任务各自可见性独立。
4. 配置对比总览
| 维度 | 需求 | 开发任务 | 外部工单 |
|---|---|---|---|
| 可见范围 | internal | internal | external |
| 人工卡点数 | 2(PRD评审、技术评审) | 0 | 1(待用户确认) |
| auto 节点 | 开发中(children_done) | 无(MVP 手动;后续 pr_merged) | 处理中(related_task_done,后续外部接入) |
| 子任务挂载 | 开发中 ← 开发任务 | 无 | 无 |
| 跨类型关联 | 无 | 作为需求的子任务 | 同步创建内部任务 |
| 终态 | 已完成 | 已完成 | 已完成 |
3. 核心流程
0. 全流程总览
四个阶段:
- 创建任务类型(管理员,组织级,低频)
- 创建任务(用户,按类型表单渲染)
- 推进任务(按出边的 trigger_mode、operator_policy 与 guard 流转)
- 任务完成(关闭 → 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 | 典型场景 |
|---|---|---|---|---|---|
manual | manual | 用户 | 用户手动选择流转 | 否(同步) | 待开发→开发中 |
auto | auto(可声明受限手动例外) | 系统 | auto_trigger 条件满足(子任务/关联任务完成;PR 合并属于后续集成) | 是(异步幂等) | 开发中→测试中(children_done,MVP) |
human_gate | human_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_approverschema 提供该字段并通过组织成员校验。 - 普通
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_RELATION | TASK_TYPE.default_version_id / TASK_TYPE_ACTIVE_VERSION / TASK_TYPE_VERSION / STATE_NODE(渲染表单) |
| 推进任务 | TASK(current_state_id)/ AUDIT_LOG | TASK.type_version_id / STATE_NODE / STATE_TRANSITION / TASK_RELATION(auto 校验) |
| 任务完成 | TASK(status/archived_at)/ 各类 *_ARCHIVE | TASK / 各类 *_ARCHIVE(按需) |