## [ERR-20260427-002] NestJS controller `@SkipTransform()` + 前端 axios 拦截器 unwrap 不兼容

**日期**: 2026-04-27
**类别**: Backend ↔ Frontend 契约
**严重度**: 高（整个页面功能失效）

### 问题描述
新建的 `AdpSyncController` 使用 `@SkipTransform()` + `@Res() res` 模式，直接 `res.json(payload)` 返回扁平对象。前端调用后 toast 报：
```
加载 PTO 列表失败: Cannot read properties of undefined (reading 'items')
```
而 curl 直接打这个 endpoint 看响应又"是对的"（扁平 JSON）。

### 根因
项目全局 axios response interceptor:
```ts
return response.data.data;  // 假定后端响应是 { success, data: T }
```
但 `@SkipTransform()` 让 controller 绕过全局 `TransformInterceptor`，HTTP body 是 `{items, total, ...}` 而不是 `{success, data: {items, total, ...}}`。
→ 拦截器返回 `response.data.data` = `undefined.items` 调用爆炸。

### 修复
**默认不要用 `@SkipTransform()`**。除非确实需要返回原始流（SSE / 文件下载），都让 controller 返回 plain object，由全局 transformer 包裹：
```ts
@Controller('adp-sync')
@UseGuards(JwtAuthGuard)  // 不加 @SkipTransform
export class AdpSyncController {
  @Get('status')
  async getStatus(@Request() req) {
    this.requireMeetingAdmin(req);
    return this.scheduler.getStatus();  // 普通对象，不要 @Res() / res.json()
  }
}
```
错误用 `throw new BadRequestException(...)` 而不是 `res.status(400).json(...)`。

### 启示
- 新加 controller 时**首选 plain return**，跟项目 90% 的 controller 保持一致
- `@SkipTransform()` 是为 SSE / blob download 准备的逃生口，不是默认选项
- 加 `@SkipTransform` 前先确认前端 api-client 是否会自动 unwrap —— 这个项目会
- 集成测试覆盖了后端响应内容（curl 看着对），但**遗漏了前端 axios 解包后的样子** → 提示日后 L0c 响应快照校验要把 axios interceptor 也算进去

---
