# Prisma aggregate 与 raw SQL 对 `undefined` 过滤值的语义不一致

**日期**: 2026-05-16
**触发**: ai-usage dashboard `summary` 接口对 itadmin 返回 825M tokens；同一时间窗 `trend` / `session-stats` / `daily-user-matrix` 返回 0/空。Root cause 调试半小时才定位。

## 不直观的关键点

Prisma 标准 query API 对 `where: { organizationId: undefined }` 的处理是**静默丢掉这条过滤**：

```ts
// 当 scope.organizationId === undefined 时：
prisma.aiUsageEvent.aggregate({
  where: { organizationId: undefined, ts: { gte, lte } },
});
// 等价于
prisma.aiUsageEvent.aggregate({
  where: { ts: { gte, lte } },  // 无 org 过滤，跨 org 全量
});
```

但 raw SQL `$queryRaw` 把 `undefined` 当成 SQL 的 `NULL`，且 `col = NULL::uuid` 永远是 `FALSE`：

```ts
// undefined → NULL；SELECT ... WHERE organization_id = NULL::uuid AND ... → 0 rows
this.prisma.$queryRaw`
  ... WHERE organization_id = ${scope.organizationId}::uuid AND ...
`;
```

## 现实触发场景

itadmin 登录后 `req.user.currentOrganizationId` 可能是 `undefined`（无当前 org 上下文）。`summary` 因为走 Prisma 标准 query，silently 跨 org 返回 825M tokens；`trend` 等走 raw SQL 则返回空。结果是仪表盘**部分图有数据、部分图无数据**——很难一眼看出是同一个 bug。

## 修复模式：`orgFilterSql` helper

```ts
private orgFilterSql(scope: BaseScope, alias = ''): Prisma.Sql {
  if (!scope.organizationId) return Prisma.empty;
  const col = alias ? `${alias}.organization_id` : 'organization_id';
  return Prisma.sql`${Prisma.raw(col)} = ${scope.organizationId}::uuid AND`;
}
```

用法（注意 AND 在 helper 末尾，配合 `WHERE ${helper}\n  ts BETWEEN ...`）：

```ts
this.prisma.$queryRaw`
  ... WHERE ${this.orgFilterSql(scope)}
    ts BETWEEN ${range.from} AND ${range.to}
    ${userFilter}
  GROUP BY ...
`;
```

跨 org 全量返回是这场景的预期语义（与 Prisma 标准 query 对齐），但**正确做法**应该是在 controller 层校验 `organizationId` 必须存在——本次先匹配既有 `summary` 的行为，留 follow-up。

## 适用范围

任何模块的 dashboard / 聚合 service 在 raw SQL 混用 Prisma 标准 query 时都该警惕：
- `feedback` dashboard（CLAUDE.md 已提及类似 DataScope 不覆盖 raw SQL 的坑）
- 后续任何写 `$queryRaw` 的报表

通用解决：把"undefined → 跳过过滤"包成 helper，不要让每个 query 自己 `if(undefined)`。

## 检测方法

L1 测试用了 `setupAiUsageTestContext` 强制注入 `organizationId`，所以 happy path 永远绿；但生产 itadmin 无 org 时翻车。建议加一个 L1 case：**scope.organizationId 缺失时验证 trend / sessionStats / dailyUserMatrix 与 summary 的 totalTokens 一致**（要么都返回全量，要么都返回 0；不能一个有、一个没有）。

## 关联文件

- 修复：`backend/src/modules/ai-usage/services/dashboard.service.ts:77-81`（helper）+ 10 个 raw SQL 调用点
- 调试：通过 Playwright MCP 直接 fetch token bearer 才发现单接口返回 200/empty；UI 层无报错很容易误判
