# Prisma upsert `update: {}` 静默保留旧字段——下游依赖 create-time 值会 FK 漂移

> **日期**: 2026-05-14
> **类型**: Prisma 数据访问陷阱

## 现象

internal-app-platform issueToken 失败：
```
Foreign key constraint violated on the constraint:
  internal_app_employee_tokens_employee_slug_fkey
```

复现步骤：
1. 早期手工 SQL 给 itadmin 用户建了 binding，slug = `lijian`（PoC 初始数据）
2. 后来调 issueToken({ userId: <itadmin>, mailNickname: 'itadmin', ... }）
3. 期望：upsert 创建/复用 binding，颁发 token
4. 实际：500 INTERNAL_SERVER_ERROR, FK 违反

## 根因

issueToken 内部逻辑：
```ts
const employeeSlug = this.slugSvc.normalizeEmployeeSlug(mailNickname); // = 'itadmin'

await tx.employeeSlugBinding.upsert({
  where: { userId },
  create: { ..., employeeSlug, ... },   // 创建路径用 'itadmin'
  update: {},                            // 已有路径不动
});

await tx.internalAppEmployeeToken.create({
  data: { employeeSlug, ... },           // 用本地变量 'itadmin'
});
```

问题：`upsert` 用 `where: { userId }` 命中**已有** binding（slug=`lijian`）：
- 走 `update: {}` 路径——什么字段都不改
- 本地变量 `employeeSlug = 'itadmin'` 仍是新算的值，与 DB 真实 slug `lijian` **错位**
- 后续 token.create 用 `employeeSlug='itadmin'` 试图 FK 到 bindings 表 → 表里只有 `lijian`，没有 `itadmin` → FK 违反 500

**Prisma upsert 不返回"实际使用的"字段——你需要自己从返回值 select 真实状态。**

## 类比 / 易踩场景

- **slug / handle / username 类终身冻结字段**：业务规定一次写入不变（这里 employee_slug 就是），用户多次接入时 derived value 可能跟 DB 不一致
- **schema 迁移期**：老数据有 legacy slug，新逻辑生成 slug 时算法变化
- **手工/脚本初始化的"种子数据"**：跟运行时算法不完全等价（本次正是种子用了 lijian 而代码算 itadmin）

## 解决

总是从 upsert 返回值取真实字段，而不是用本地"输入"变量：

```ts
const binding = await tx.employeeSlugBinding.upsert({
  where: { userId },
  create: { ..., employeeSlug: computedSlug, ... },
  update: {},
  select: { employeeSlug: true, organizationId: true },  // ← 关键
});

// 后续用 binding.employeeSlug，不用 computedSlug
await tx.internalAppEmployeeToken.create({
  data: { employeeSlug: binding.employeeSlug, ... },
});
```

## 适用面

任何 `upsert` + `update: {}`（或部分 update）+ 下游依赖 create-time 字段的场景。
判别 checklist：
1. upsert 有 `update: {}` 或不覆盖所有字段？
2. 后续代码用了"本地变量"而不是 upsert 返回值？
3. 那个字段是 unique key 或被 FK 引用？

任一条命中 → 改成"取 upsert 返回 + 后续都用返回值"。

## 状态

- ✅ token.service.ts:62 `issue()` 改为从 upsert.select 拿真实 slug + organizationId
- ✅ 单测 82/82 仍过（mock 端未触发此分支，集成测试触发了真问题）
- 经验：**集成测试 > 单元测试**——这种 FK + 跨字段一致性 bug 单测永远抓不到
