# Controller 手工 `{success,data}` 包装会被全局 TransformInterceptor 再包一层 → 双包装坑

> **日期**: 2026-05-14
> **类型**: NestJS 全局 interceptor 项目约定 / 前后端契约面陷阱
> **来源**: internal-app-platform 前端「我的 Apps」页接 backend 时 console
>          `TypeError: Cannot read properties of undefined (reading 'length')`

## 现象

internal-app-platform controller 写法：
```ts
return { success: true, data: { apps } };
```

curl 看到的实际响应：
```json
{
  "success": true,
  "data": {
    "success": true,
    "data": { "apps": [...] }
  },
  "timestamp": "...",
  "path": "..."
}
```

**双层 `success/data` 嵌套**——前端 apiClient 默认抽 `response.data`，拿到的是
内层那个 `{ success: true, data: { apps } }`；再访问 `.apps` 是 undefined →
渲染时 `apps.length` 炸 TypeError。

## 根因

backend `main.ts:113` 注册了**全局 TransformInterceptor**：

```ts
app.useGlobalInterceptors(loggingInterceptor, new TransformInterceptor(), ...);
```

TransformInterceptor 把 controller 任意返回值 X 自动包成：
```ts
{ success: true, data: X, message: 'success', timestamp: ..., path: ... }
```

所以 controller **只该返回业务 payload**，**不要**自己加 `{success, data}` 包装。
项目里其它老 controller（feedback、approval、iam）都直接 `return entity` 或
`return { items, total }`，从来不手工包。这是隐式项目约定。

新写 controller 不知道这层约定时，会自然以为 "API 返回应该是 `{success, data}`"
然后手工包，得到双包装，前端炸。

## 现象快速判定

console 报 `Cannot read properties of undefined (reading 'X')` + 后端有 `success/data` 全局
拦截器 → 直接 curl 一下端点看是否 `response.data.success === true && response.data.data.success === true` 双层为 true。是 → 命中本坑。

## 修法

controller 里**所有 `return { success: true, data: X }` 改成 `return X`**。错误路径用
`throw new HttpException(...)` 让 AllExceptionsFilter 统一处理（同样不要自己包错误体）。

```ts
// ❌ 错的（双包装）
return { success: true, data: { apps } };

// ✅ 对的
return { apps };

// ❌ 错的（错误也双包装）
return { success: false, error: { code: '...', message: '...' } };

// ✅ 对的
throw new UnauthorizedException({ code: '...', message: '...' });
```

## 适用面

- **任何新 controller**：写返回前先 grep 全局 interceptor 看是否自动包
- **集成/迁移老 API**：从其它框架/项目搬代码时最容易踩
- **前后端契约对齐**：CLAUDE.md §6 "前后端契约对齐"——controller 返回 type 应该
  跟前端 interface **直接对齐**，不要中间套 wrapper

## 项目其它地方的同款坑

如果有别的 controller 手工返 `{ success, data }`，前端调时会出同样双包装。检查命令：
```bash
grep -rn 'return.*{.*success:\s*true.*data:' backend/src/modules/ | grep -v auth
```
（auth 模块可能确实需要自定义返回结构，例外。）

## 状态

- ✅ internal-app-platform.controller.ts 4 个端点全部去掉手工包装
- ✅ 错误路径改用 BadRequestException / UnauthorizedException
- ✅ 在 controller 文件顶部加注释提醒后来者
- 经验：今后给项目加全局 interceptor 时**必须**写文档说明 controller 应该怎么返回，
  否则新模块（特别是 AI 起草的）很容易踩
