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:
@@ -42,6 +42,19 @@ export function getJson<T>(url: string, params?: Record<string, unknown>): Promi
|
||||
return http.get(url, { params }) as unknown as Promise<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed JSON request helper for non-GET methods (PATCH/POST/PUT/DELETE). Same
|
||||
* response-unwrapping contract as `getJson`. Used by the LOCAL write endpoints
|
||||
* (e.g. voice_clip 手动匹配).
|
||||
*/
|
||||
export function requestJson<T>(
|
||||
method: 'patch' | 'post' | 'put' | 'delete',
|
||||
url: string,
|
||||
data?: unknown,
|
||||
): Promise<T> {
|
||||
return http.request({ method, url, data }) as unknown as Promise<T>
|
||||
}
|
||||
|
||||
/** 以 blob 下载(HTML 导出等附件)。从 Content-Disposition 取文件名(绕开 http 的 (r)=>r.data 解包拦截器)。 */
|
||||
export async function downloadBlob(
|
||||
url: string,
|
||||
|
||||
+22
-1
@@ -3,7 +3,7 @@
|
||||
// Mirrors the FastAPI routes in yunke-server/app/api/routers/. Each wrapper
|
||||
// drops `undefined`/empty params so the query string only carries set filters
|
||||
// (the backend treats missing as "no filter").
|
||||
import { getJson } from './client'
|
||||
import { getJson, requestJson } from './client'
|
||||
import type {
|
||||
Account,
|
||||
CallChannel,
|
||||
@@ -228,6 +228,27 @@ export function getVoiceClips(query: VoiceClipQuery = {}): Promise<Page<VoiceCli
|
||||
)
|
||||
}
|
||||
|
||||
/** PATCH /voice-clips/{id}/match 响应(镜像 backend payload)。 */
|
||||
export interface VoiceClipMatchResult {
|
||||
voice_clip_id: number
|
||||
matched_friend_id: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动匹配 voice_clip ↔ customer (LOCAL 写)。`friendId=null` 取消匹配。
|
||||
* 该写操作只落本地镜像库,不回写云客。
|
||||
*/
|
||||
export function matchVoiceClip(
|
||||
voiceClipId: number,
|
||||
friendId: string | null,
|
||||
): Promise<VoiceClipMatchResult> {
|
||||
return requestJson<VoiceClipMatchResult>(
|
||||
'patch',
|
||||
`/voice-clips/${voiceClipId}/match`,
|
||||
{ friend_id: friendId },
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SMS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -175,6 +175,18 @@ export interface VoiceClip {
|
||||
duration_format?: string | null
|
||||
/** `wechat` / `whatsapp` — local tag set at scrape time (not sent upstream). */
|
||||
channel: string
|
||||
/**
|
||||
* LOCAL-only 手动匹配: the customer.friend_id this clip has been associated
|
||||
* with, or null when unmatched. Set via PATCH /voice-clips/{id}/match; never
|
||||
* pushed back to 云客. (spec §7)
|
||||
*/
|
||||
matched_friend_id?: string | null
|
||||
/**
|
||||
* Display name for `matched_friend_id`, resolved by the backend via Customer
|
||||
* (remark > nickname > real_name > friend_id). null when unmatched. Not a
|
||||
* stored column — computed by the GET handler.
|
||||
*/
|
||||
matched_friend_name?: string | null
|
||||
/** Verbatim upstream call/list record (camelCase keys). */
|
||||
raw_json?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user