feat(calls): voice_clip 手动匹配(voice_clip↔customer 本地关联写操作)

- VoiceClip: 新增 matched_friend_id 列 (String(128), nullable) + alembic 迁移 b3c4d5e6f7a8
- PATCH /api/voice-clips/{id}/match: friend_id 关联 / null 取消 (本地写, 不回写云客)
- GET /api/voice-clips: 批量解析 matched_friend_name (remark>nickname>real>id)
- VoiceClipModal: 已匹配客户列 + 匹配/取消匹配按钮 + 嵌套客户选择 picker (默认按员工列客户, 名搜索走 /search)
- 测试: 17 passed (新增 7 个 match 用例覆盖 set/clear/name 解析/404)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-11 10:43:34 +08:00
parent f1b5861b35
commit 9da272b992
9 changed files with 533 additions and 21 deletions
+7
View File
@@ -277,6 +277,13 @@ class VoiceClipOut(BaseModel):
oss_file_name: str | None = None
mp3_duration: int | None = None
duration_format: str | None = None
# LOCAL-only 手动匹配 (spec §7): the customer.friend_id this clip has been
# associated with, or None when unmatched. ``matched_friend_name`` is NOT a
# stored column — it's resolved by the GET handler via a Customer lookup
# (remark > nickname > real_name > friend_id) so the modal can render the
# 「已匹配客户」 column without a second round-trip per row.
matched_friend_id: str | None = None
matched_friend_name: str | None = None
raw_json: dict[str, Any] | None = None