Skip to content

approvals: a department approver never resolves when the business unit has organization_id = null (every seeded BU) #3807

Description

@os-zhuang

问题

department 审批人在真机上恒解析为空,落到死值 department:<id> —— 只要目标 sys_business_unit 行的 organization_idnull,而审批请求带着组织 id。这正是 seed 出来的组织架构的常态。

expandBusinessUnitUsers(packages/plugins/plugin-approvals/src/approval-service.ts:806)第一步是「种子健全性检查」:

const seed = await this.engine.find('sys_business_unit', {
  filter: organizationId
    ? { id: businessUnitId, organization_id: organizationId }   // ← 这里
    : { id: businessUnitId },
  fields: ['id', 'active'], limit: 1, context: SYSTEM_CTX,
});
const seedRow = Array.isArray(seed) ? seed[0] : null;
if (!seedRow || seedRow.active === false) return [];

BU 行 org 为 null、请求 org 为默认组织 → 等值比较不命中 → return [] → 上层落到 [${a.type}:${a.value}] 死值。BFS 子节点那一段(L828-830)有同样的等值过滤。

为什么这在真实应用里必然发生

seed 写不出运行时才创建的默认组织 id。showcase 的 src/data/seed/index.ts:199-204 就是这么写的(5 个 BU 全部不带 organization_id),运行时 AuthPlugin 再建出 Default Organization。于是:

  • 设计器 Department 选择器列出的 5 个 BU 全部organization_id = null;
  • 作者选任意一个,审批请求都路由到没有人的槽位。

配合 #3508 刚落地的记录 lookup,这条路现在更容易被走到——以前是手填 id,现在是从选择器里点一下就得到一个死槽位。

复现(真机,showcase + sqlite)

examples/app-showcase $ PORT=3000 pnpm exec objectstack dev --seed-admin -p 3000
  1. POST /api/v1/data/sys_business_unit_member { business_unit_id: 'bu_hq_finance', user_id: 'usr_showcase_phone_demo' }
  2. 建一个带 { type: 'department', value: 'bu_hq_finance' } 审批人的 flow,触发它。
  3. sys_approval_request.pending_approversdepartment:bu_hq_finance(死值,不是 usr_showcase_phone_demo)。

同一条流水线上另外三种记录型审批人都正常解析(user → id、team → 成员、position → 机器名的持有者),只有 department 落空。

因果已证实:把 BU 的 org 补上再触发一次(换一条记录以绕开 pending 去重),同一个 flow 立刻解析到正确的人:

PATCH /api/v1/data/sys_business_unit/bu_hq_finance { organization_id: 'org_ms48pbgs4lelo6q8' }

前: usr_showcase_auditor_demo, <admin>, department:bu_hq_finance, <ops holder>
后: usr_showcase_auditor_demo, <admin>, usr_showcase_phone_demo,  <ops holder>

同一文件里的口径不一致

  • expandTeamUsers(L791)完全不按 org 过滤;
  • sys_business_unit_member 的成员查询(L839)也不按 org 过滤;
  • 只有 BU 自身的种子检查 + BFS 子节点按 org 等值过滤。

另外 isOverrideActor 的注释已经确立了平台口径:「a null-org request is global」。按同样的语义,org 为 null 的 BU 应当是全局可见的,而不是对任何带 org 的请求都不可见。

建议

二选一(倾向 A):

A. 引擎把 null-org 当全局——种子检查与 BFS 过滤改成「本组织 ∪ 无组织」:

filter: organizationId
  ? { id: businessUnitId, $or: [{ organization_id: organizationId }, { organization_id: null }] }
  : { id: businessUnitId }

expandTeamUsers 的宽松口径、以及 isOverrideActor 的 null-org=global 语义一致。

B. seed/bootstrap 给平台对象补 org 戳——但 seed 阶段拿不到运行时组织 id,得挪到 kernel:bootstrapped 之后补写,成本更高且每个宿主应用都要各自照做。

无论选哪条,department 目前属于「声明了、选得到、运行时不兑现」——正是 Prime Directive #10 要在生产者侧大声报错的那一类。

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions