479 lines
29 KiB
Markdown
479 lines
29 KiB
Markdown
# M4 联系人管理 — Chatwoot 功能梳理文档
|
||
|
||
> 基于 Chatwoot 源码深度阅读 + CodeGraph 上下文梳理
|
||
> 生成日期:2026-05-22
|
||
> 参照仓库:chatwoot-reference
|
||
|
||
---
|
||
|
||
## 模块概述
|
||
|
||
本模块覆盖 Chatwoot 的联系人(Contact)全生命周期管理,包括联系人 CRUD、搜索、合并、ContactInbox(联系人-收件箱关联)、CustomAttribute(自定义属性)、CustomFilter(自定义筛选器)、Note(笔记)、Label(标签)以及企业版 Company(公司)功能。核心目标是构建完整的 CRM 体系,让客服团队可以对客户信息进行精细化管理、分类和检索。
|
||
|
||
---
|
||
|
||
## 1. 联系人 CRUD + 状态管理
|
||
|
||
### 联系人创建
|
||
- **功能描述**:联系人(Contact)是 Chatwoot CRM 的核心实体,代表一个客户/潜在客户。创建时可指定邮箱、电话、姓名等基本信息,并可同时关联到一个 Inbox(通过 ContactInbox)。联系人通过渠道消息自动创建,也可由坐席手动创建。
|
||
- **用户操作流程**:
|
||
1. 客户通过渠道(WhatsApp/Facebook/Telegram/WebWidget 等)发消息,系统自动创建联系人 + ContactInbox
|
||
2. 坐席可在 Contacts 页面手动创建联系人(`POST /api/v1/accounts/{account_id}/contacts`),可同时指定 `inbox_id` 和 `source_id`
|
||
3. CSV 批量导入联系人(`POST /api/v1/accounts/{account_id}/contacts/import`),仅管理员可操作
|
||
4. 创建后触发 `dispatch_create_event`(ActionCable)和 `ip_lookup`(如 feature flag 启用)
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/contacts` — 创建联系人
|
||
- 请求参数:`{ name, identifier, email, phone_number, avatar, avatar_url, inbox_id, source_id, additional_attributes: {}, custom_attributes: {} }`
|
||
- `POST /api/v1/accounts/{account_id}/contacts/import` — CSV 导入(管理员)
|
||
- `POST /api/v1/accounts/{account_id}/contacts/export` — CSV 导出(管理员)
|
||
- 响应:联系人对象 `{ id, name, email, phone_number, identifier, avatar_url, additional_attributes, custom_attributes, contact_type, blocked, ... }`
|
||
- **涉及的数据模型**:
|
||
- **Contact**(`contacts` 表)核心字段:
|
||
- `id, name, email, phone_number, identifier, middle_name, last_name, country_code, location, contact_type, blocked`
|
||
- `additional_attributes`(jsonb)— 存放 city, company_name, social_* 等扩展信息
|
||
- `custom_attributes`(jsonb)— 存放自定义属性键值对
|
||
- `account_id, company_id`(企业版), `last_activity_at, created_at, updated_at`
|
||
- `contact_type` enum:`visitor(0), lead(1), customer(2)`
|
||
- 索引:
|
||
- `index_contacts_on_account_id_and_contact_type` — 按账户+类型查询
|
||
- `index_contacts_on_lower_email_account_id` — 邮箱大小写不敏感唯一
|
||
- `index_contacts_on_name_email_phone_number_identifier` — GIN 全文搜索
|
||
- `index_contacts_on_nonempty_fields` — 过滤有标识信息的联系人
|
||
- **涉及的业务逻辑**:
|
||
- **ContactInboxBuilder**(`app/builders/contact_inbox_builder.rb`):创建联系人时若指定 inbox_id,自动创建 ContactInbox。根据渠道类型智能生成 source_id(Email→邮箱、WhatsApp→去掉+的电话、Twilio→带+的电话、WebWidget/API→UUID)
|
||
- **Contacts::SyncAttributes**(`app/services/contacts/sync_attributes.rb`):
|
||
- 从 `additional_attributes` 同步 `location` 和 `country_code` 到主字段
|
||
- 自动升级 `contact_type`:visitor → lead(当有 email/phone/social 信息时)
|
||
- **Avatarable** concern:自动从 Gravatar 拉取头像(邮箱变更时异步执行)
|
||
- `before_validation :prepare_contact_attributes` — 清理空属性
|
||
- `before_save :sync_contact_attributes` — 同步联系人属性
|
||
- `after_create_commit :dispatch_create_event, :ip_lookup`
|
||
- `after_update_commit :dispatch_update_event`
|
||
- `after_destroy_commit :dispatch_destroy_event`
|
||
- **涉及的权限策略**:
|
||
- **ContactPolicy**:`index?, active?, search?, filter?, show?, create?, update?, contactable_inboxes?, destroy_custom_attributes?, avatar?` → 所有坐席可操作;`import?, export?, destroy?` → 仅管理员
|
||
- **涉及的事件/通知**:
|
||
- ActionCable:创建/更新/删除事件推送
|
||
- `CONTACT_MERGED` 事件(合并时触发)
|
||
|
||
### 联系人列表 + 排序 + 分页
|
||
- **功能描述**:支持按账户获取联系人列表,支持多字段排序和分页。默认只展示有标识信息的"resolved contacts"(CRM V2 模式下只展示 lead 类型)。
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{account_id}/contacts` — 分页列表(默认 15 条/页)
|
||
- `GET /api/v1/accounts/{account_id}/contacts/active` — 最近活跃联系人
|
||
- 请求参数:`page, sort, include_contact_inboxes`
|
||
- **排序支持(Sift gem)**:
|
||
- `email, name, phone_number, last_activity_at, created_at, company_name, city, country` — 支持 asc/desc
|
||
- **resolved_contacts 逻辑**:
|
||
- 默认模式:`WHERE email <> '' OR phone_number <> '' OR identifier <> ''`
|
||
- CRM V2 模式:`WHERE contact_type = 'lead'`
|
||
- **标签过滤**:支持 `labels` 参数(ActsAsTaggableOn `tagged_with`,any: true)
|
||
|
||
### 联系人搜索
|
||
- **功能描述**:对联系人的 name/email/phone_number/identifier 进行 ILIKE 模糊搜索,返回分页结果。
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{account_id}/contacts/search?q=xxx` — 搜索
|
||
- 响应包含 `has_more` 标识(多取一条判断是否还有更多数据)
|
||
|
||
### 联系人更新
|
||
- **涉及的API端点**:
|
||
- `PUT /api/v1/accounts/{account_id}/contacts/{id}` — 更新联系人
|
||
- 更新时 custom_attributes 和 additional_attributes 采用 merge(合并)策略而非覆盖
|
||
- 支持 `avatar_url` 参数,从 URL 拉取头像
|
||
|
||
### 联系人删除
|
||
- **涉及的API端点**:
|
||
- `DELETE /api/v1/accounts/{account_id}/contacts/{id}` — 删除联系人(仅管理员)
|
||
- 删除前检查在线状态(OnlineStatusTracker),在线联系人不可删除
|
||
- 关联的 conversations, contact_inboxes, messages, notes 等通过 `dependent: :destroy_async` 异步级联删除
|
||
|
||
### 联系人头像管理
|
||
- **涉及的API端点**:
|
||
- `DELETE /api/v1/accounts/{account_id}/contacts/{id}/avatar` — 删除头像
|
||
|
||
---
|
||
|
||
## 2. 联系人高级筛选(Filter)
|
||
|
||
### 筛选器运行
|
||
- **功能描述**:基于 `Contacts::FilterService`,支持多条件组合筛选联系人。筛选条件来自 `filter_keys.yml` 中的 contact 字段定义 + 自定义属性。支持标准属性、additional_attributes 和自定义属性三种类型。
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/contacts/filter` — 执行筛选
|
||
- 请求参数:`{ payload: { contacts: [ { attribute_key, filter_operator, values, attribute_type, data_type } ... ] } }`
|
||
- **筛选字段(filter_keys.yml → contacts)**:
|
||
| 字段 | attribute_type | data_type | filter_operators |
|
||
|------|---------------|-----------|-----------------|
|
||
| name | standard | text_case_insensitive | equal_to, not_equal_to, contains, does_not_contain |
|
||
| phone_number | standard | text | equal_to, not_equal_to, contains, does_not_contain, starts_with |
|
||
| email | standard | text_case_insensitive | equal_to, not_equal_to, contains, does_not_contain |
|
||
| identifier | standard | text_case_insensitive | equal_to, not_equal_to |
|
||
| country_code | additional_attributes | text_case_insensitive | equal_to, not_equal_to |
|
||
| city | additional_attributes | text_case_insensitive | equal_to, not_equal_to, contains, does_not_contain |
|
||
| company_name | additional_attributes | text_case_insensitive | equal_to, not_equal_to, contains, does_not_contain |
|
||
| labels | standard | labels | equal_to, not_equal_to, is_present, is_not_present |
|
||
| created_at | standard | date | is_greater_than, is_less_than, days_before |
|
||
| last_activity_at | standard | date | is_greater_than, is_less_than, days_before |
|
||
| blocked | standard | boolean | equal_to, not_equal_to |
|
||
| *(自定义属性)* | custom_attribute | 对应类型 | 对应操作符 |
|
||
- **涉及的业务逻辑**:
|
||
- **Contacts::FilterService** 继承 **FilterService**:
|
||
- `base_relation` → `account.contacts.resolved_contacts`
|
||
- 支持 `phone_number` 特殊处理(自动加 +)
|
||
- 支持 `country_code` 特殊处理(自动 downcase)
|
||
- 支持 `crm_v2` feature flag
|
||
- FilterService 核心逻辑:解析 filter payload → 逐条件构建 SQL → 组合 `query_string` + `filter_values` → 执行查询
|
||
- 自定义属性筛选通过 `Filters::CustomAttributeFilterHelper`,查询 `custom_attribute_definitions` 表获取属性定义,动态构建 JOIN + WHERE 条件
|
||
- **涉及的权限策略**:
|
||
- ContactPolicy `filter?` → 所有坐席可用
|
||
|
||
---
|
||
|
||
## 3. 联系人合并(Merge)
|
||
|
||
### 合并操作
|
||
- **功能描述**:将两个联系人合并为一个。base_contact 保留为主联系人,mergee_contact 的数据被合并后删除。合并操作在事务中执行,迁移所有关联数据。
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/actions/contact_merge` — 合并联系人
|
||
- 请求参数:`{ base_contact_id, mergee_contact_id }`
|
||
- **涉及的数据模型 + 关键字段**:无独立数据模型,纯 Action 类
|
||
- **涉及的业务逻辑**:
|
||
- **ContactMergeAction**(`app/actions/contact_merge_action.rb`):
|
||
1. `validate_contacts` — 校验两个联系人属于同一账户
|
||
2. `merge_conversations` — 将 mergee 的所有 Conversation 的 contact_id 改为 base
|
||
3. `merge_messages` — 将 mergee 作为 sender 的所有 Message 改为 base
|
||
4. `merge_contact_inboxes` — 将 mergee 的所有 ContactInbox 改为 base
|
||
5. `merge_contact_notes` — 将 mergee 的所有 Note 改为 base
|
||
6. `merge_and_remove_mergee_contact` — 合并属性策略:
|
||
- 可合并字段:`identifier, name, email, phone_number, additional_attributes, custom_attributes`
|
||
- 使用 `deep_merge`:base_contact 的值优先,mergee 的空值被跳过
|
||
- 合并后删除 mergee_contact
|
||
7. 触发 `CONTACT_MERGED` 事件(Dispatcher dispatch)
|
||
- **涉及的权限策略**:
|
||
- ContactMergesController 继承 BaseController,需要 `check_authorization`
|
||
- **注意事项**:
|
||
- 如果 base_contact.id == mergee_contact.id,直接返回 base_contact(防止重复合并)
|
||
- 合并是事务性操作,失败则回滚
|
||
|
||
---
|
||
|
||
## 4. ContactInbox(联系人-收件箱关联)
|
||
|
||
### ContactInbox 创建
|
||
- **功能描述**:ContactInbox 是联系人与收件箱的中间关联表,记录联系人在某个渠道的外部标识(source_id)。一个联系人可以关联多个 Inbox(多渠道联系)。
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/contacts/{contact_id}/contact_inboxes` — 创建 ContactInbox
|
||
- 请求参数:`{ inbox_id, source_id }`
|
||
- **涉及的数据模型**:
|
||
- **ContactInbox**(`contact_inboxes` 表)核心字段:
|
||
- `id, contact_id, inbox_id, source_id, hmac_verified, pubsub_token`
|
||
- 索引:`inbox_id + source_id` UNIQUE(同一收件箱+source唯一)
|
||
- `pubsub_token` UNIQUE(用于 ActionCable 推送)
|
||
- **涉及的业务逻辑**:
|
||
- **ContactInboxBuilder**(`app/builders/contact_inbox_builder.rb`):
|
||
- 若已有相同 inbox_id + source_id 的 ContactInbox,直接返回(不重复创建)
|
||
- 若未指定 source_id,根据渠道类型自动生成:
|
||
- Email → 使用 `contact.email`
|
||
- WhatsApp → 使用 `contact.phone_number`(去掉 +)
|
||
- Twilio SMS → 根据渠道 medium 生成(SMS→带+的电话,WhatsApp→去掉+)
|
||
- SMS → 使用 `contact.phone_number`
|
||
- WebWidget/API → `SecureRandom.uuid`
|
||
- 支持 `hmac_verified` 参数(Website 渠道 HMAC 校验标识)
|
||
- `valid_source_id_format?` — 校验 source_id 格式(使用 RegexHelper)
|
||
- `current_conversation` — 返回该 ContactInbox 的最新对话
|
||
- `webhook_data` — 包含 contact/inbox/account/current_conversation 的完整推送数据
|
||
- **ContactableInboxes(可用收件箱)**:
|
||
- `GET /api/v1/accounts/{account_id}/contacts/{id}/contactable_inboxes` — 获取该联系人可以发消息的收件箱列表
|
||
- **Contacts::ContactableInboxesService**:遍历账户所有 Inbox,根据渠道类型和联系人信息判断可用性:
|
||
- Email → 需要有 email
|
||
- WhatsApp/SMS/Twilio → 需要有 phone_number
|
||
- WebWidget → 需要已有 ContactInbox 且无活跃对话
|
||
- API → 直接可用(可用已有 source_id 或生成 UUID)
|
||
|
||
### ContactInbox 清理(Stale Cleanup)
|
||
- **功能描述**:系统定期清理没有关联对话且创建时间过久的 ContactInbox 和无标识信息的联系人。
|
||
- **涉及的业务逻辑**:
|
||
- `ContactInbox.stale_without_conversations(time_period)` — scope 查找无对话的旧 ContactInbox
|
||
- `Contact.stale_without_conversations(time_period)` — scope 查找无标识、无对话的旧联系人
|
||
- `Internal::RemoveStaleContactInboxesJob/Service` — 定期清理
|
||
- `Internal::RemoveStaleContactsJob/Service` — 定期清理
|
||
|
||
---
|
||
|
||
## 5. CustomAttributeDefinition(自定义属性定义)
|
||
|
||
### 自定义属性定义 CRUD
|
||
- **功能描述**:自定义属性定义(CustomAttributeDefinition)是账户级别的配置,定义可用于 Contact/Conversation/Company 的扩展字段。每个定义指定属性键、显示名称、显示类型、所属模型等。定义后,对应实体的 `custom_attributes` jsonb 字段即可存储该键值对。
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{account_id}/custom_attribute_definitions` — 按模型类型列出定义
|
||
- `GET /api/v1/accounts/{account_id}/custom_attribute_definitions/{id}` — 获取单个定义
|
||
- `POST /api/v1/accounts/{account_id}/custom_attribute_definitions` — 创建定义
|
||
- `PUT /api/v1/accounts/{account_id}/custom_attribute_definitions/{id}` — 更新定义
|
||
- `DELETE /api/v1/accounts/{account_id}/custom_attribute_definitions/{id}` — 删除定义
|
||
- **涉及的数据模型**:
|
||
- **CustomAttributeDefinition**(`custom_attribute_definitions` 表)核心字段:
|
||
- `id, attribute_display_name, attribute_description, attribute_key, attribute_display_type, attribute_model, attribute_values(jsonb), default_value, regex_pattern, regex_cue, account_id`
|
||
- `attribute_model` enum:`conversation_attribute(0), contact_attribute(1), company_attribute(2)`
|
||
- `attribute_display_type` enum:`text(0), number(1), currency(2), percent(3), link(4), date(5), list(6), checkbox(7)`
|
||
- 索引:`attribute_key + attribute_model + account_id` UNIQUE
|
||
- **标准属性(不可覆盖)**:
|
||
- Contact 标准属性:`name, email, phone_number, identifier, country_code, city, company_name, created_at, last_activity_at, referer, blocked`
|
||
- Conversation 标准属性:`status, priority, assignee_id, inbox_id, team_id, display_id, campaign_id, labels, browser_language, country_code, referer, created_at, last_activity_at`
|
||
- Company 标准属性:`name, domain, description, contacts_count, created_at, updated_at, last_activity_at`
|
||
- **涉及的校验**:
|
||
- `attribute_must_not_conflict` — attribute_key 不能与标准属性冲突(on :create)
|
||
- `normalize_attribute_fields` — attribute_key 自动 downcase
|
||
- `regex_pattern/regex_cue` — 企业版扩展,支持正则校验提示
|
||
- `attribute_values` — list 类型时存储可选值列表
|
||
- **涉及的业务逻辑**:
|
||
- `after_update :update_widget_pre_chat_custom_fields` — 更新时同步到 Widget 的预聊天自定义字段(非 company_attribute)
|
||
- `after_destroy :sync_widget_pre_chat_custom_fields` — 删除时同步
|
||
- 企业版 **Enterprise::Concerns::CustomAttributeDefinition**:
|
||
- `after_destroy :cleanup_conversation_required_attributes` — 删除定义后清理 `account.conversation_required_attributes`
|
||
- **涉及的权限策略**:
|
||
- **CustomAttributeDefinitionPolicy**:`index?, show?` → 坐席+管理员;`create?, update?, destroy?` → 仅管理员
|
||
|
||
### 联系人自定义属性操作
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/contacts/{id}/destroy_custom_attributes` — 删除指定的自定义属性键
|
||
- 请求参数:`{ custom_attributes: ["key1", "key2"] }`
|
||
- 实际是排除(excluding)指定键,保留其余
|
||
- 更新联系人时通过 `custom_attributes: {}` 参数 merge 到已有属性
|
||
|
||
---
|
||
|
||
## 6. CustomFilter(自定义筛选器)
|
||
|
||
### 自定义筛选器 CRUD
|
||
- **功能描述**:自定义筛选器(CustomFilter)是用户级别的筛选条件保存机制,允许坐席保存常用筛选条件以便快速复用。每个筛选器属于一个用户+账户,可按类型区分(对话/联系人/报告)。
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{account_id}/custom_filters?filter_type=contact` — 列出用户的联系人筛选器
|
||
- `GET /api/v1/accounts/{account_id}/custom_filters/{id}` — 获取单个筛选器
|
||
- `POST /api/v1/accounts/{account_id}/custom_filters` — 创建筛选器
|
||
- `PUT /api/v1/accounts/{account_id}/custom_filters/{id}` — 更新筛选器
|
||
- `DELETE /api/v1/accounts/{account_id}/custom_filters/{id}` — 删除筛选器
|
||
- **涉及的数据模型**:
|
||
- **CustomFilter**(`custom_filters` 表)核心字段:
|
||
- `id, name, filter_type, query(jsonb), account_id, user_id`
|
||
- `filter_type` enum:`conversation(0), contact(1), report(2)`
|
||
- **涉及的校验**:
|
||
- `validate_number_of_filters` — 每用户每账户最多 `Limits::MAX_CUSTOM_FILTERS_PER_USER` 个筛选器
|
||
- **创建筛选器请求**:
|
||
- `{ custom_filter: { name: "VIP Contacts", filter_type: "contact", query: { contacts: [{ attribute_key: "email", filter_operator: "contains", values: ["@vip.com"] }] } } }`
|
||
|
||
---
|
||
|
||
## 7. Note(笔记)
|
||
|
||
### 笔记 CRUD
|
||
- **功能描述**:笔记(Note)是联系人级别的内部备忘信息,供坐席记录与客户相关的备注。笔记按创建时间倒序排列。
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{account_id}/contacts/{contact_id}/notes` — 列出笔记(倒序)
|
||
- `GET /api/v1/accounts/{account_id}/contacts/{contact_id}/notes/{id}` — 获取单条笔记
|
||
- `POST /api/v1/accounts/{account_id}/contacts/{contact_id}/notes` — 创建笔记
|
||
- `PUT /api/v1/accounts/{contact_id}/accounts/{account_id}/contacts/{contact_id}/notes/{id}` — 更新笔记
|
||
- `DELETE /api/v1/accounts/{account_id}/contacts/{contact_id}/notes/{id}` — 删除笔记
|
||
- **涉及的数据模型**:
|
||
- **Note**(`notes` 表)核心字段:
|
||
- `id, content(text, not null), account_id, contact_id, user_id, created_at, updated_at`
|
||
- `user_id` optional — 可匿名笔记
|
||
- **涉及的业务逻辑**:
|
||
- `before_validation :ensure_account_id` — 自动从 contact 取 account_id
|
||
- `scope :latest` → `order(created_at: :desc)`
|
||
- 创建时自动注入 `user_id: Current.user.id`
|
||
- 删除联系人时级联删除(`dependent: :destroy_async`)
|
||
- 合并联系人时笔记迁移到 base_contact
|
||
|
||
---
|
||
|
||
## 8. Label(标签)
|
||
|
||
### 标签 CRUD
|
||
- **功能描述**:标签(Label)是账户级别的分类标记,可用于联系人和对话。标签通过 ActsAsTaggableOn 实现多对多关联。标签有颜色、描述、是否在侧边栏显示等属性。
|
||
- **涉及的API端点**(账户级别标签管理):
|
||
- `GET /api/v1/accounts/{account_id}/labels` — 列出标签
|
||
- `GET /api/v1/accounts/{account_id}/labels/{id}` — 获取标签
|
||
- `POST /api/v1/accounts/{account_id}/labels` — 创建标签
|
||
- `PUT /api/v1/accounts/{account_id}/labels/{id}` — 更新标签
|
||
- `DELETE /api/v1/accounts/{account_id}/labels/{id}` — 删除标签
|
||
- **涉及的数据模型**:
|
||
- **Label**(`labels` 表)核心字段:
|
||
- `id, title, color(default "#1f93ff"), description, show_on_sidebar, account_id`
|
||
- 索引:`title + account_id` UNIQUE
|
||
- **涉及的校验**:
|
||
- title:presence, format(UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE),uniqueness(scope: account_id)
|
||
- `before_validation` → title 自动 downcase
|
||
- **涉及的业务逻辑**:
|
||
- `acts_as_taggable_on :labels`(通过 Labelable concern)
|
||
- `Labelable` concern 方法:
|
||
- `update_labels(labels)` — 设置标签列表(覆盖)
|
||
- `add_labels(new_labels)` — 添加标签(追加)
|
||
- `after_update_commit :update_associated_models` — 标题变更时触发 `Labels::UpdateJob`,异步更新所有使用旧标签名的 taggings
|
||
- 删除标签时触发 `Labels::RemoveAssociationsJob`,清理所有 taggings
|
||
- `default_scope { order(:title) }` — 默认按标题排序
|
||
|
||
### 联系人标签操作
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/contacts/{contact_id}/labels` — 设置/更新联系人标签
|
||
- `GET /api/v1/accounts/{account_id}/contacts/{contact_id}/labels` — 获取联系人标签列表
|
||
- 请求参数:`{ labels: ["vip", "enterprise"] }`
|
||
- 使用 LabelConcern:`model.update_labels(permitted_params[:labels])`
|
||
|
||
### 联系人批量标签操作
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/bulk_actions` — 批量操作(type=Contact)
|
||
- 请求参数:`{ type: "Contact", ids: [...], labels: { add: ["vip"] } }`
|
||
- **Contacts::BulkAssignLabelsService**:批量给多个联系人添加标签
|
||
|
||
---
|
||
|
||
## 9. Company(企业版)
|
||
|
||
### 公司 CRUD
|
||
- **功能描述**:公司(Company)是企业版功能,代表联系人所属的企业组织。联系人通过 email 域名自动关联到公司,也可手动关联。Company 拥有自己的 custom_attributes、avatar、description 等。
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{account_id}/companies` — 公司列表(分页 25 条/页)
|
||
- `GET /api/v1/accounts/{account_id}/companies/search?q=xxx` — 搜索公司(name/domain)
|
||
- `GET /api/v1/accounts/{account_id}/companies/{id}` — 获取公司详情
|
||
- `POST /api/v1/accounts/{account_id}/companies` — 创建公司
|
||
- `PUT /api/v1/accounts/{account_id}/companies/{id}` — 更新公司
|
||
- `DELETE /api/v1/accounts/{account_id}/companies/{id}` — 删除公司(仅管理员)
|
||
- `POST /api/v1/accounts/{account_id}/companies/{id}/destroy_custom_attributes` — 删除自定义属性
|
||
- `DELETE /api/v1/accounts/{account_id}/companies/{id}/avatar` — 删除公司头像
|
||
- 需 `ensure_companies_enabled!` → feature flag `companies` 必须启用
|
||
- **涉及的数据模型**:
|
||
- **Company**(`companies` 表)核心字段:
|
||
- `id, name(not null, length limit), domain(unique scope: account_id, 格式校验), description(text, length limit), additional_attributes(jsonb), custom_attributes(jsonb), contacts_count, last_activity_at, account_id`
|
||
- 索引:`account_id + domain` UNIQUE WHERE domain IS NOT NULL
|
||
- **涉及的业务逻辑**:
|
||
- **Company** include Avatarable → 支持 avatar + Gravatar
|
||
- `before_validation :prepare_jsonb_attributes` — 清理 jsonb
|
||
- `after_create_commit :fetch_favicon` — domain 存在时异步拉取 favicon
|
||
- `has_many :contacts, dependent: :nullify` — 删除公司时联系人 company_id 设为 NULL
|
||
- scope:`ordered_by_name`, `search_by_name_or_domain`
|
||
|
||
### 公司-联系人关联
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{account_id}/companies/{company_id}/contacts` — 公司下的联系人列表
|
||
- `GET /api/v1/accounts/{account_id}/companies/{company_id}/contacts/search?q=xxx` — 在公司下搜索联系人
|
||
- `POST /api/v1/accounts/{account_id}/companies/{company_id}/contacts` — 将联系人关联到公司
|
||
- `DELETE /api/v1/accounts/{account_id}/companies/{company_id}/contacts/{contact_id}` — 从公司中移除联系人
|
||
- **自动关联逻辑**:
|
||
- **Enterprise::Concerns::Contact**:
|
||
- `after_commit :associate_company_from_email, on: [:create, :update], if: :should_associate_company?`
|
||
- 条件:`email.present? && company_id.nil? && saved_change_to_email? && saved_change_to_email.first.nil?`
|
||
- **Contacts::CompanyAssociationService**:
|
||
- 检查邮箱是否为商业邮箱(`Companies::BusinessEmailDetectorService`)
|
||
- 提取域名 → `find_or_create_by!(account, domain)` 创建或查找公司
|
||
- 公司名称从 `additional_attributes.company_name` 或域名推导
|
||
- 关联后更新 `contact.company_id` + `Company.contacts_count`(counter_cache)
|
||
- `after_update_commit :record_company_activity` — 联系人活跃时更新公司 `last_activity_at`
|
||
|
||
---
|
||
|
||
## 10. 批量操作
|
||
|
||
### 批量删除联系人
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/bulk_actions` — `{ type: "Contact", action_name: "delete", ids: [...] }`
|
||
- 权限检查:删除操作需 ContactPolicy `destroy?`(仅管理员)
|
||
- **涉及的业务逻辑**:
|
||
- **Contacts::BulkActionService** → 调度到 `Contacts::BulkActionJob`
|
||
- `delete_requested?` → `Contacts::BulkDeleteService` → `find_each(&:destroy!)`
|
||
- **涉及的数据模型**:无独立模型
|
||
|
||
---
|
||
|
||
## 11. 导入/导出
|
||
|
||
### CSV 导入
|
||
- **功能描述**:支持通过 CSV 文件批量导入联系人。仅管理员可操作。
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/contacts/import` — 导入 CSV
|
||
- 如企业版未启用联系人导入功能,返回错误
|
||
|
||
### CSV 导出
|
||
- **功能描述**:支持将联系人导出为 CSV 文件,异步执行完成后通过邮件发送给管理员。
|
||
- **涉及的API端点**:
|
||
- `POST /api/v1/accounts/{account_id}/contacts/export` — 导出
|
||
- **涉及的业务逻辑**:
|
||
- **Account::ContactsExportJob**:
|
||
- 异步生成 CSV,支持自定义列名
|
||
- Labels 列特殊处理(preload + join taggings)
|
||
- 生成后通过 `AdministratorNotifications::MailService` 发送邮件通知
|
||
|
||
---
|
||
|
||
## 12. 联系人对话查看
|
||
|
||
### 获取联系人的对话
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{account_id}/contacts/{contact_id}/conversations` — 获取联系人的所有对话
|
||
|
||
### 获取联系人的附件
|
||
- **涉及的API端点**:
|
||
- `GET /api/v1/accounts/{contact_id}/accounts/{account_id}/contacts/{contact_id}/attachments` — 获取联系人相关附件
|
||
|
||
---
|
||
|
||
## 13. 关系图谱总结
|
||
|
||
```
|
||
Account
|
||
├── Contact (belongs_to, N:1)
|
||
│ ├── ContactInbox (has_many, N:N through contact_inboxes)
|
||
│ │ ├── Inbox (belongs_to)
|
||
│ │ ├── Conversation (has_many)
|
||
│ │ └── Pubsubable (pubsub_token)
|
||
│ ├── Conversation (has_many)
|
||
│ ├── Message (has_many, as: sender)
|
||
│ ├── Note (has_many)
|
||
│ ├── CsatSurveyResponse (has_many)
|
||
│ ├── Labels (acts_as_taggable_on :labels)
|
||
│ ├── Avatar (has_one_attached)
|
||
│ ├── Company (belongs_to, 企业版, counter_cache)
|
||
│ ├── custom_attributes (jsonb)
|
||
│ └── additional_attributes (jsonb)
|
||
├── Company (企业版, has_many contacts)
|
||
├── CustomAttributeDefinition (has_many)
|
||
├── CustomFilter (has_many)
|
||
└── Label (has_many)
|
||
```
|
||
|
||
---
|
||
|
||
## 14. 跨模块依赖
|
||
|
||
| 依赖模块 | 依赖内容 |
|
||
|----------|---------|
|
||
| M1 账户与用户 | Contact/Company/Label/CustomAttributeDefinition 均属于 Account;ContactPolicy/CustomAttributeDefinitionPolicy 依赖 AccountUser 角色 |
|
||
| M2 收件箱与渠道 | ContactInbox 连接 Contact ↔ Inbox;ContactInboxBuilder 根据渠道类型生成 source_id;ContactableInboxesService 查询可用渠道 |
|
||
| M3 对话与消息 | Contact has_many Conversations;Contact 是 Message 的 polymorphic sender;合并时迁移 Conversation 和 Message |
|
||
| M5 团队与分配 | Contact 不直接关联 Team(通过 Conversation 间接关联) |
|
||
| M7 通知与事件 | CONTACT_MERGED 事件经 Dispatcher dispatch;contact.create/update/destroy 事件推送 ActionCable |
|
||
|
||
---
|
||
|
||
## 15. GoChat 实现建议
|
||
|
||
### P0(核心必备)
|
||
1. **Contact CRUD**:联系人创建/读取/更新/删除,含 email/phone/name/identifier 基本字段 + additional_attributes/custom_attributes jsonb
|
||
2. **ContactInbox**:联系人-收件箱关联,含 source_id,创建时按渠道类型智能生成
|
||
3. **Contact 搜索**:ILIKE 模糊搜索(name/email/phone/identifier)
|
||
4. **Contact 状态类型**:visitor/lead/customer 三级进阶,自动升级逻辑
|
||
5. **Note CRUD**:联系人笔记,含 content + user_id + timestamps
|
||
|
||
### P1(重要增强)
|
||
6. **Label 系统**:账户级标签 CRUD + 联系人/对话标签关联(可使用简化版 taggable)
|
||
7. **Contact 合并**:base + mergee 合并操作,迁移所有关联数据
|
||
8. **Contact 筛选**:多条件组合筛选(标准属性 + 自定义属性)
|
||
9. **CustomAttributeDefinition**:自定义属性定义 CRUD(contact/conversation/company 三类)
|
||
10. **CustomFilter**:用户级筛选条件保存和复用
|
||
11. **批量操作**:批量删除 + 批量标签赋值
|
||
12. **ContactableInboxes**:查询联系人可用的收件箱列表
|
||
|
||
### P2(企业版扩展)
|
||
13. **Company**:公司 CRUD + 自动关联(email 域名检测)
|
||
14. **Company-Contact**:公司下的联系人管理 + 移除关联
|
||
15. **Stale Cleanup**:定期清理无对话/无标识的 ContactInbox 和 Contact
|
||
16. **Contact 导入/导出**:CSV 批量导入 + 异步导出邮件通知
|
||
17. **regex_pattern/regex_cue**:自定义属性正则校验 |