wangjianlong_0626
451aa4e915
fix(profile): ProfileAssemblyService 漏写 current_customer_id(唯一键失效的另一处)
## 现象(2026-09-12 端到端跑通后查数据时发现)
客户 9102 的画像快照状态**彼此矛盾**:
| version | is_current | current_customer_id |
|---|---|---|
| 2 | `0` | `9102` ← 清旧时没清空 |
| 3 | `1` | `NULL` ← 建新时没写入 |
## 根因
`ProfileAssemblyService._write_snapshot` 把 `current_customer_id` **当成了生成列**:
- 方法 docstring 原文写着"唯一键 `uk_profile_snapshot_current` 建立在**生成列** `current_customer_id` 上"
- 因此两处都不赋值(以为 DB 会自动填)
**但该列不是生成列** —— `alembic/baseline_generated.sql` 与真实库都是**普通可空列 + 唯一键**,
`app/model/profile.py` 的模块 docstring 第 2 条已明确:"当前版本必须由写入方**显式写入**客户 ID
(历史版本写 NULL),才能保证「每个客户最多一条当前快照」"。
后果:唯一键**形同虚设**(多个 NULL 不冲突)⇒ 不变式失效;且旧版本残留的值
一旦与新版本补上的值相同,就会**直接撞唯一键**。
> 这与本线先前修的 `CustomerProfileCandidateService._write_profile_snapshot` 是**同一个缺陷的另一处**
> —— 当时只找到一处,这次是靠真实链路跑出数据后核对才暴露出来。
## 改动
`app/service/profile_assembly_service.py`:
- 旧版本:`is_current = False` 的同时 `current_customer_id = None`
- 新版本:`is_current=True` 的同时 `current_customer_id=customer_id`
- 订正方法 docstring 的错误认知("生成列"→ 普通可空列 + 唯一键),并写明后果
## 已有数据订正
新增 `tools/fix_profile_snapshot_current.py`(**默认 dry-run**、幂等、`--apply` 才提交):
1. 先清空 `is_current=0` 却残留值的行
2. 再补写 `is_current=1` 却是 NULL 的行
3. **顺序要紧**:反过来的话第 2 步会与残留值撞唯一键
本机实测:清空 1 行、补写 1 行,复核两类异常均归零。
## 验证
- `mypy app` → 0 错 / 245 文件
- `pytest tests`(全量)→ `2 failed, 1418 passed, 1 skipped`
(2 个失败为既有环境项:httpx 把中文序列化成 `\uXXXX`,非本次引入)
- 端到端:真实对话 → 记忆抽取 → `memory_unit` 落库已实测通过(客户 9102
`preference:risk_level = "低风险"`,候选态)
2026-09-12 14:20:38 +08:00
..
2026-09-12 13:14:57 +08:00
2026-09-09 21:55:37 +08:00
2026-09-11 17:26:18 +08:00
2026-09-11 20:28:21 +08:00
2026-09-12 13:14:57 +08:00
2026-09-11 22:31:51 +08:00
2026-09-11 15:08:37 +08:00
2026-09-11 14:12:56 +08:00
2026-09-11 16:42:31 +08:00
2026-09-11 20:50:41 +08:00
2026-09-09 21:55:37 +08:00
2026-09-11 17:32:47 +08:00
2026-09-11 16:57:47 +08:00
2026-09-12 13:14:57 +08:00
2026-09-11 14:46:40 +08:00
2026-09-11 16:11:30 +08:00
2026-09-11 16:11:30 +08:00
2026-09-11 16:11:30 +08:00
2026-09-11 14:46:40 +08:00
2026-09-11 14:44:29 +08:00
2026-09-10 09:23:22 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 16:12:38 +08:00
2026-09-10 21:43:39 +08:00
2026-09-12 12:50:14 +08:00
2026-09-09 21:55:37 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 13:11:58 +08:00
2026-09-10 18:42:44 +08:00
2026-09-11 22:31:51 +08:00
2026-09-11 14:46:40 +08:00
2026-09-11 14:46:40 +08:00
2026-09-10 22:08:15 +08:00
2026-09-12 11:15:24 +08:00
2026-09-11 18:42:42 +08:00
2026-09-11 16:57:47 +08:00
2026-09-10 20:22:42 +08:00
2026-09-11 19:47:14 +08:00
2026-09-12 14:11:42 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 22:31:51 +08:00
2026-09-11 16:57:47 +08:00
2026-09-12 12:50:14 +08:00
2026-09-09 21:55:37 +08:00
2026-09-11 20:15:17 +08:00
2026-09-12 12:20:01 +08:00
2026-09-12 12:50:14 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 16:57:47 +08:00
2026-09-10 09:23:22 +08:00
2026-09-09 21:55:37 +08:00
2026-09-11 16:42:31 +08:00
2026-09-11 19:20:32 +08:00
2026-09-11 12:47:01 +08:00
2026-09-11 18:39:57 +08:00
2026-09-11 12:47:01 +08:00
2026-09-11 16:42:31 +08:00
2026-09-12 14:20:38 +08:00
2026-09-12 10:45:40 +08:00
2026-09-11 16:42:31 +08:00
2026-09-10 21:55:03 +08:00
2026-09-10 21:58:42 +08:00
2026-09-09 21:55:37 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 17:26:18 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 22:31:51 +08:00
2026-09-11 21:22:56 +08:00
2026-09-11 14:14:50 +08:00
2026-09-10 21:03:44 +08:00
2026-09-10 21:03:44 +08:00
2026-09-10 21:03:44 +08:00
2026-09-11 13:39:50 +08:00
2026-09-11 14:16:07 +08:00
2026-09-11 11:42:53 +08:00
2026-09-11 14:24:45 +08:00
2026-09-10 21:03:44 +08:00
2026-09-11 15:25:45 +08:00
2026-09-11 16:40:15 +08:00
2026-09-11 12:57:07 +08:00
2026-09-11 09:49:21 +08:00
2026-09-11 14:24:45 +08:00
2026-09-11 19:37:26 +08:00
2026-09-11 21:17:47 +08:00
2026-09-11 16:57:47 +08:00
2026-09-11 21:03:58 +08:00
2026-09-11 16:57:47 +08:00