---
date: 2026-05-11
tags: [api, organization, contract-trap, frontend]
status: documented
---

# `/departments/organizations` 不返回 Organization —— 命名陷阱

## 现象

新组织架构网格视图（`/organization/structure/grid`）调用 `getTopLevelOrganizations()`，
拿到一个 id 后传给 `getDepartmentTree(orgId)`，结果 tree 返回空数组。

后端日志显示请求路径 `GET /api/v1/departments/tree?organizationId=22222222-...`
返回 200，但 body 是空。

## 根因

`getTopLevelOrganizations()` 函数名误导。它实际调的是
`GET /api/v1/departments/organizations`（**挂在 departments controller 下**），
返回的是 **顶级部门（parent_id IS NULL 的 Department）**，
**不是真正的 corp_hr.organizations 表里的 Organization 实体**。

```ts
// frontend/src/services/api/organization.ts:847
export async function getTopLevelOrganizations(regionId?: string): Promise<Department[]> {
  return apiClient.get('/departments/organizations', { params: { regionId } });
}
```

返回类型已经写了 `Department[]`，但函数名 `getTopLevelOrganizations` 让人以为
拿的是 Organization。把它的 id 当作 `organizationId` 传给 `/departments/tree?organizationId=...`，
后端 `WHERE org_id = '<dept-id>'` 找不到任何 dept → 空数组。

旧 Canvas 页 [structure/page.tsx](frontend/src/app/(modules)/organization/structure/page.tsx)
也用同样的 `getTopLevelOrganizations()` 当"组织视角"。Canvas 页能工作可能是因为它的渲染逻辑容忍
空结果 / 用顶级部门本身作为节点起点（不强制依赖 tree response）。但任何新代码再走这条路都会重蹈覆辙。

## 正确做法

要"先选组织、再取该组织的部门树"，调真正的 organizations endpoint：

```ts
import { getOrganizations, getDepartmentTree } from '@/services/api/organization';

const { items } = await getOrganizations({ limit: 100 });  // 真 Organization[]
const tree = await getDepartmentTree(items[0].id);          // 用 org.id ✓
```

## 为什么不直接改 `getTopLevelOrganizations`

- 这个函数被多处使用（旧 Canvas 页等），改返回类型属于契约性变更，影响面大
- 改名同理会破坏调用方
- 短期：新代码改用 `getOrganizations()`；中期：等专门修这条历史契约时再统一

## 触发条件

- 新页面/新组件，需要"按组织过滤部门"
- 直觉地用 `getTopLevelOrganizations()` 拿"组织"
- 把返回的 id 传给任何接受 `organizationId` 的下游 API

## 检查清单

下次开发组织/部门相关功能前：
1. 确认你需要的是 **Organization**（独立的法人/组织实体）还是 **顶级 Department**（部门树的根节点）
2. `Organization` → `getOrganizations()` / `/api/v1/organizations`
3. 顶级 `Department`（用作"视角"或"perspective"）→ `getTopLevelOrganizations()` / `/api/v1/departments/organizations`
4. 别把两者混用
