2026-07-30-oa-cc-multi-assignee-design.md 9.5 KB

抄送人节点 · 多种选人方式并集(钉钉式)

日期: 2026-07-30
范围:

  • web-admin 审批设计器「3 流程设计」· 抄送人节点配置(FlowDesigner.vue / flow-tree/types.ts)
  • backend 发布校验(oa_publish.py)、审批人解析(oa_assignee.py)、引擎建任务 / 预览 / 进度(oa_engine.py)
  • 发起预览中「发起人自选」判定与人员并集(OaFlowPreview 等相关前端)

关联:

  • docs/superpowers/specs/2026-07-14-oa-approval-dingtalk-design.md §5.3(选人策略)
  • docs/superpowers/specs/2026-07-23-oa-initiator-select-in-flow-design.md(抄送自选可空)
  • 产品对照:钉钉审批 · 抄送人可同时启用多种选人方式,结果取并集

1. 背景与目标

现状

  • 审批人 / 抄送人节点共用 approverConfig.assigneeType(单值),设计器为分段单选。
  • 五种方式:users | oa_roles | dept_leader | direct_supervisor | initiator_select。
  • 引擎 resolve_assignees 按单一类型硬分支;失败则抛 AssigneeResolveError。
  • 抄送特例:仅当 assigneeType === initiator_select 且发起人未选人时,跳过建任务且不报错。

目标

  1. 仅抄送人节点支持同时勾选多种选人方式,运行时对各方式解析结果并集去重后建抄送任务。
  2. 运行时:软跳过失败/空策略;最终 0 人时不报错、不建任务(对齐现有「发起人自选抄送可空」)。
  3. 发布时:至少勾选 1 种;勾了 users / oa_roles 则必须已配置成员/角色。
  4. 兼容已发布/草稿中仅含旧字段 assigneeType 的抄送配置,行为与现网一致。

非目标

  • 不改审批人节点(仍单选一种 + 或签/会签)。
  • 不新增数据库表或列(流程定义仍为 JSON)。
  • 不改变抄送任务「建即 done / 不阻断流程」语义。
  • 不做历史实例数据回填迁移脚本(只读兼容即可)。

2. 产品约定(已确认)

项 结论
范围 仅抄送人;审批人保持单选
运行时合并 各勾选方式独立解析 → 人 ID 并集去重(保序:按 assigneeTypes 顺序,同策略内保持原序)
单策略失败/空 软跳过该策略,继续其它策略
最终 0 人 不报错,不创建抄送任务,流程继续
发布校验 必须勾选 ≥1 种;users / oa_roles 若勾选则配置不可空;dept_leader / direct_supervisor / initiator_select 勾选即可
取消勾选 UI 可不清空已填 userIds / oaRoleIds;发布只校验当前勾选的策略
发起人自选并存 仍按节点 ➕ 选人;自选列表可空;与固定策略结果并集

3. 配置模型

3.1 类型(前端 ApproverConfig)

export interface ApproverConfig {
  /** 审批人:单选(必填)。抄送人:兼容旧数据 / 可选双写首项 */
  assigneeType?: AssigneeType
  /** 抄送人:多选选人方式(优先)。审批人:不使用 */
  assigneeTypes?: AssigneeType[]
  userIds?: string[]
  oaRoleIds?: string[]
  oaRoleCodes?: string[]
  signMode?: 'or' | 'and' // 仅审批有意义;抄送引擎侧仍固定 cc
}

3.2 写入约定(设计器保存抄送节点)

  • 写入非空 assigneeTypes(去重、仅含合法枚举、保持勾选顺序)。
  • 双写 assigneeType = assigneeTypes[0],便于未改完的只读展示兜底。
  • userIds / oaRoleIds 始终可随表单保留;解析时仅当对应类型在 assigneeTypes 中才使用。

3.3 读取约定(后端 / 前端统一 helper)

对节点 approverConfig:

  1. 若 assigneeTypes 为非空数组 → 过滤非法项后作为类型列表(抄送多选)。
  2. 否则若存在非空 assigneeType → [assigneeType]。
  3. 否则 → [](发布校验会拦;运行时抄送视为 0 人)。

审批节点:忽略 assigneeTypes(即使误写入),只认 assigneeType。


4. 解析与引擎

4.1 oa_assignee.py

保留现有 resolve_assignees(单策略、失败抛错)供审批使用。

新增(名称可微调,语义如下):

resolve_cc_assignees(...) -> (user_ids: List[str], meta: Dict)

行为:

  1. 用 §3.3 得到 types。
  2. 对每个 t in types:构造单策略临时 config,调用现有单策略逻辑(或内联相同分支)。
  3. try/except AssigneeResolveError:捕获则跳过该策略,并在 meta.skipped 记录 { type, reason }。
  4. 单策略返回空列表:视为跳过(不抛错)。
  5. 合并去重;meta.resolve_source 建议:
    • 仅一种成功:该策略名(兼容旧展示)
    • 多种成功:"multi",并带 sources: [...]
  6. 永不因「全部失败」抛错;返回 ([], meta) 合法。

initiator_select:仍使用调用方传入的 initiator_select_user_ids;空则该策略跳过。

4.2 oa_engine.py · _create_tasks_for_node(及预览同类路径)

对 ntype == "cc":

  • 调用 resolve_cc_assignees(不再先特判「仅 initiator_select 且空则 return」——该语义被多策略软跳过覆盖)。
  • 若 assignees 为空 → return created(不抛错)。
  • 非空 → 按现逻辑为每人建 task_type=cc、status=done 任务。

对 ntype == "approval":行为不变(resolve_assignees + 硬失败;自选空仍抛错)。

预览 / 进度未达节点解析抄送人员:同样走 resolve_cc_assignees,空列表展示为无人抄送(不阻断预览)。

4.3 发起预览前端

判断节点是否可 ➕ 自选:

  • 旧:assigneeType === 'initiator_select'
  • 新:类型列表(§3.3)包含 initiator_select

固定策略解析出的人只读展示;自选人仍写入按节点 map,提交后与固定人并集由引擎完成。


5. 发布校验(oa_publish.py)

对 type === "cc":

条件 结果
类型列表为空 错误:未配置抄送人策略
含非法类型 错误
含 users 且 userIds 为空 错误:指定成员不能为空
含 oa_roles 且角色 id/code 皆空 错误:工作流角色不能为空
仅含 dept_leader / direct_supervisor / initiator_select 通过

对 type === "approval":保持现有单 assigneeType 校验,不启用 assigneeTypes。


6. 设计器 UI

6.1 抄送人抽屉

  • 「选人方式」分段控件改为多选:再次点击已选可取消(取消后若变为 0 种,确定保存前前端提示至少一种;发布后端再拦一道)。
  • 勾选 users 时展示指定成员区;勾选 oa_roles 时展示角色区;可同时展开。
  • 副标题可改为:「可同时选择多种方式,抄送时合并去重」。

6.2 审批人抽屉

  • 保持单选;逻辑与现网一致。

6.3 画布摘要

  • 抄送:多种时拼接标签,如 指定成员 + 部门负责人;过长可截断。
  • 审批:不变。

7. 兼容与迁移

场景 行为
旧抄送仅有 assigneeType 读作单元素列表;运行/发布与现网一致
新抄送有 assigneeTypes 按多选;双写首项 assigneeType
旧运行中实例 已落库任务不受影响;未达节点若重解析则走新逻辑(旧 JSON 无 assigneeTypes 时仍单策略)
审批节点误带 assigneeTypes 引擎/发布忽略,只认 assigneeType

无需 SQL patch。


8. 主要改动文件

路径 变更
web-admin/.../flow-tree/types.ts assigneeTypes;摘要文案;默认抄送配置
web-admin/.../flow-tree/FlowDesigner.vue 抄送多选 UI;读写;校验提示
web-admin/.../OaFlowPreview.vue(及同类) includes('initiator_select')
web-admin/.../OaProgressChain.vue(若展示策略名) 支持 multi / 多标签
backend/app/services/oa_assignee.py resolve_cc_assignees
backend/app/services/oa_publish.py 抄送多类型校验
backend/app/services/oa_engine.py 抄送建任务/预览走新解析
backend/tests/(若有 OA 测例) 并集、软跳过、0 人、发布校验、旧字段兼容

9. 测试要点

  1. 兼容:旧单 assigneeType=users 抄送,指定成员仍抄送成功。
  2. 并集:勾选指定成员 A + 部门负责人 B(且 B 有效)→ 两人收到抄送。
  3. 软跳过:勾选部门负责人(未配置)+ 有效指定成员 → 仅抄送指定成员,发起/推进不报错。
  4. 0 人:仅勾发起人自选且发起不选人 → 不建抄送任务,流程可发起/可推进。
  5. 全失败:仅勾部门负责人且未配置 → 不建任务、不报错。
  6. 发布:勾选指定成员但未选人 → 发布失败;至少勾一种通过。
  7. 审批回归:审批人仍单选;多人会签/或签/自选必填行为不变。
  8. 发起 ➕:抄送 assigneeTypes 含 initiator_select 时节点仍可点选加人。

10. 开放细节(实现时可定,不阻规格)

  • meta.resolve_source === "multi" 时进度文案具体用「多种选人方式」还是拼接中文标签——实现时与现有 OaProgressChain 文案表对齐即可。
  • 设计器取消最后一种选人方式时:禁止取消 vs 允许取消但确定时 ElMessage 拦截——推荐后者与发布校验一致。

修订记录

日期 说明
2026-07-30 初稿:仅抄送多选;软跳过+允许 0 人;发布至少一种且 users/roles 配置要齐;assigneeTypes + 旧字段兼容 + 双写首项