Flow 4:studio-api 写入详情¶
Lambda: studio-api(studio-website-monorepo) · 触发: 员工 UI 操作 · 状态: 部分实施(2026-05-04)
实施溯源 + 落地状态(2026-05-04)
历史 doc 写"DEFERRED — studio-api 当前只连 DynamoDB,不连 Neon"已过时。当前状态:
- 已 Neon-connected:用
@neondatabase/serverlessHTTP driver 直接读写 Neon(raw SQL,不用 Drizzle helper) - 已实施 4a Close:UPDATE tasks 关闭 + 写
task_events审计表 - 未实施 4a 完整原子 batch:不写
contact_timeline、不更新contacts.lastActivityAt、不发 SQS 给 contacts-analyzer - 未实施 4b Postpone:暂无 postpone 端点
- 额外端点:reopen(重开 task)、note(改备注)、events、communications、list(读取)
Studio-api 额外写入:PUT /v3/calls/:id/staff 修正 staff 归属时,写 calls.original_staff_name + calls.amendment_history(详见 per-call-analysis-writes.md Step ①.5)。
数据流概览¶
不调用 AI,所有值来自员工 UI 操作或系统硬编码。
员工 UI 操作 (Web)
│
├─────────────────────────────┬─────────────────────┐
│ │ │
┌────▼─────────┐ ┌────▼─────┐ ┌─────▼─────┐
│ 4a Close │ │ Reopen │ │ 4b Postpone│
│ ✅ 已实施 │ │ ✅ 已实施 │ │ ❌ 未实施 │
└────┬─────────┘ └──────────┘ └───────────┘
│
┌────▼─────────────────────────────────────┐
│ Neon: UPDATE tasks │
│ status='closed' + close_type │
│ closed_at=NOW() + closed_by_staff_name │
│ close_result + close_note │
│ updated_at=NOW() │
│ WHERE store-level filter (#214) │
└────┬─────────────────────────────────────┘
│
┌────▼─────────────────────────────┐
│ Neon: INSERT task_events │
│ eventType='task_closed' │
│ actorUserId + actorStaffName │
│ metadata={close_result, note} │
└──────────────────────────────────┘
│
❌ 不写 contact_timeline (gap)
❌ 不更新 contacts.lastActivityAt (gap)
❌ 不发 SQS → contacts-analyzer (gap,daily cron 兜底)
4a. Close(关闭 Task)— ✅ 已实施¶
员工打完电话后从 UI 关闭 task,填 closeResult + closeNote。
端点:PATCH /v2/tasks/close?taskId=<uuid>
输入¶
Query:taskId(UUID)
Body(zod-validated):
| 字段 | 类型 | 说明 |
|---|---|---|
storeId |
string min(1) |
用于权限校验 + store-level filter |
staffName |
string min(1) max(200) |
操作员工 |
closeResult |
string min(1) max(100) |
11 值枚举之一(见 tasks-field-design.md closeResult 章节) |
note |
string max(1000) optional |
备注 |
权限校验¶
- 校验 user 对该 store 有访问权(
not_found/forbidden/authorized) - 取
store.providerAccountId作account_id过滤 - 取该 store 拥有的电话号码列表,用于 store-level WHERE filter
- 拼接
WHERE store_phone IN (...) OR store_id = ?— 防止跨 store 改 task(retaintive/studio-website-monorepo#214修复)
Step ①:UPDATE tasks(原子)¶
| 字段 | 值 |
|---|---|
status |
'closed' |
close_type |
'manual_closed' |
closed_at |
NOW() |
closed_by_staff_name |
body.staffName |
close_result |
body.closeResult(11 值枚举) |
close_note |
body.note |
updated_at |
NOW() |
WHERE 条件:task_id = $taskId AND account_id = $providerAccountId AND status = 'pending' AND <store-level filter>。
响应处理:
- 0 行 → 检查 task 是否存在:不存在 → 404 NotFoundError,存在但已关 → 409 ConflictError
- 1 行 → 200 + 返回 {taskId, status, closedAt}
Step ②:INSERT task_events(审计表)¶
| 字段 | 值 |
|---|---|
task_id |
taskId |
site_id |
providerAccountId |
event_type |
'task_closed' |
actor_user_id |
userId(JWT 解析) |
actor_staff_name |
staffName |
metadata |
jsonb {close_result, close_note} |
⚠️ 当前不是原子 batch:UPDATE tasks 和 INSERT task_events 是两个独立 SQL 调用。前者成功后者失败 → task 已关但无 audit。后续若需原子化,改用
db.batch()模式。
未实施的部分(gap)¶
| 操作 | 状态 | 影响 |
|---|---|---|
INSERT contact_timeline (task.status_changed) |
❌ 未写 | 前端读 contact_timeline 看不到 manual close 事件,只能从 task_events 单独查 |
UPDATE contacts.lastActivityAt |
❌ 未写 | contacts 列表排序拿不到员工互动时间,会显得过时 |
SQS → contacts-analyzer(source: 'task_close') |
❌ 未发 | 关闭 task 不立刻触发 AI 重分析,等 daily 06:00 cron 兜底 |
上述 gap 不阻塞功能(任务可关闭),但跟设计意图(原子 batch + 立即 re-analyze)有差距。
4b. Postpone(推迟 dueAt)— ❌ 未实施¶
当前没有 postpone 端点,以下是终态设计。
设计意图(待实施)¶
客户要求改约时间(如"周四再打给我"),员工推迟 dueAt,必须填 reason。
Step ①:UPDATE tasks¶
| 字段 | 值 |
|---|---|
dueAt |
payload.newDueAt |
updatedAt |
NOW() |
API 层校验 status='pending',closed task 不可 postpone。
Step ②:INSERT contact_timeline¶
公共字段(contactPhone/franchiseId/accountId/storeId/eventCategory='task'/occurredAt=NOW()/actorType='staff')外:
| 字段 | 值 |
|---|---|
eventType |
'task.due_at_changed' |
entityType |
'task' |
entityId |
String(task.taskId) |
oldValue |
{dueAt: old_due_at} |
newValue |
{dueAt: payload.newDueAt, reason: payload.reason} |
actorName |
payload.staffName |
Step ③:不更新 contacts¶
推迟 ≠ 互动,lastActivityAt 不变。
Step ④:不触发 Contact Analysis¶
推迟不改变客户状态,不产生 Contact Analysis 需要的新信息。