Files
gochat/docs/requirements/M4-contact-management.md
T
2026-06-04 15:44:48 +08:00

479 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**:自定义属性正则校验