# V4 验收报告 — Companies模块 (2026-06-02 更新版) ## 验收标准 1. 接口路径一致 2. 请求参数一致 3. 响应格式一致 4. 错误处理与状态码一致 5. 业务逻辑一致 --- ## 1. 接口路径对比 | # | Chatwoot端点 | GoChat端点 | 状态 | |---|---|---|---| | 1 | GET /api/v1/accounts/:account_id/companies | GET /api/v1/accounts/:id/companies | ✅ 兼容 (path param命名差异不影响功能) | | 2 | POST /api/v1/accounts/:account_id/companies | POST /api/v1/accounts/:id/companies | ✅ 兼容 | | 3 | GET /api/v1/accounts/:account_id/companies/search | GET /api/v1/accounts/:id/companies/search | ✅ 兼容 | | 4 | GET /api/v1/accounts/:account_id/companies/:id | GET /api/v1/accounts/:id/companies/:company_id | ⚠️ path param名不同 — 功能兼容 | | 5 | PATCH/PUT /api/v1/accounts/:account_id/companies/:id | PUT /api/v1/accounts/:id/companies/:company_id | ⚠️ Chatwoot支持PATCH, GoChat仅PUT | | 6 | DELETE /api/v1/accounts/:account_id/companies/:id | DELETE /api/v1/accounts/:id/companies/:company_id | ⚠️ path param命名差异 | | 7 | POST /companies/:id/destroy_custom_attributes | — | ❌ **缺失** | | 8 | DELETE /companies/:id/avatar | — | ❌ **缺失** | | 9 | GET /companies/:company_id/contacts | GET /companies/:company_id/contacts | ✅ 兼容 | | 10 | GET /companies/:company_id/contacts/search | — | ❌ **缺失** | | 11 | POST /companies/:company_id/contacts | POST /companies/:company_id/contacts/:contact_id | ⚠️ **路径差异** — Chatwoot用body传contact_id, GoChat用URL param | | 12 | DELETE /companies/:company_id/contacts/:id | DELETE /companies/:company_id/contacts/:contact_id | ✅ 兼容 (path param命名差异) | | 13 | GET /companies/:company_id/conversations | GET /companies/:company_id/conversations | ✅ 兼容 | | 14 | GET /companies/:company_id/notes | GET /companies/:company_id/notes | ✅ 兼容 | ### GoChat额外端点 (Chatwoot不存在): | # | GoChat端点 | 说明 | |---|---|---| | 15 | POST /companies/:company_id/notes | Chatwoot notes只有:index,无:create — GoChat扩展 | | 16 | DELETE /companies/:company_id/notes/:note_id | Chatwoot无delete note端点 — GoChat扩展 | ### 端点汇总: - ✅ 兼容: 7个 - ⚠️ 路径差异但功能兼容: 4个 (path param命名, PATCH vs PUT) - ❌ 缺失: 3个 (destroy_custom_attributes, avatar delete, contacts/search) - ⚠️ 新增路径差异: 1个 (AddContact用URL param而非body) **结论: 接口路径 ≈80%一致, 缺失3个端点, 1个端点路径结构不同** --- ## 2. 请求参数对比 ### Create Company | 字段 | Chatwoot | GoChat | 状态 | |---|---|---|---| | name | ✅ (required, permit) | ✅ (required, validate:min=1) | ✅ 兼容 | | domain | ✅ (permit) | ✅ (json:domain) | ✅ 兼容 | | description | ✅ (permit) | ✅ (json:description) | ✅ 兼容 | | avatar | ✅ (permit — file upload) | ❌ **缺失** | | | additional_attributes | ✅ (permit: {}) | ❌ **缺失** | | | custom_attributes | ✅ (permit: {}) | ✅ (json.RawMessage) | ✅ 兼容 | ### Update Company | 字段 | Chatwoot | GoChat | 状态 | |---|---|---|---| | name | ✅ | ✅ | ✅ 兼容 | | domain | ✅ | ✅ | ✅ 兼容 | | description | ✅ | ✅ | ✅ 兼容 | | avatar | ✅ | ❌ **缺失** | | | custom_attributes | ✅ (merge逻辑) | ⚠️ 全量替换 | ❌ **行为不兼容** | | additional_attributes | ✅ | ❌ **缺失** | | ### Search | 参数 | Chatwoot | GoChat | 状态 | |---|---|---|---| | q (query string) | ✅ (required — 422 if blank) | ✅ (optional — 空query回退到list) | ⚠️ **行为差异** | | page | ✅ | ✅ | ✅ 兼容 | | sort | ✅ (Sift gem: name/domain/created_at/last_activity_at/contacts_count) | ✅ (name/domain/created_at/last_activity_at) | ⚠️ GoChat缺少contacts_count排序 | ### List (index) | 参数 | Chatwoot | GoChat | 状态 | |---|---|---|---| | page | ✅ | ✅ | ✅ 兼容 | | sort | ✅ | ✅ (同上支持) | ⚠️ 缺contacts_count排序 | | per_page | — (Chatwoot固定25) | ✅ (动态page_size) | ⚠️ Chatwoot固定25 | ### Notes (create) | 参数 | Chatwoot | GoChat | 状态 | |---|---|---|---| | content | — (Chatwoot无create) | ✅ (required) | GoChat扩展功能 | **结论: 请求参数 ≈75%一致, 缺失avatar/additional_attributes, custom_attributes更新行为不兼容** --- ## 3. 响应格式对比 ### GoChat当前响应envelope (APIResponse struct): ```json { "success": true, "data": { ...company object... }, "meta": { "page": 1, "per_page": 25, "total_count": 100 } } ``` ### Chatwoot list/search envelope: ```json { "meta": { "total_count": N, "page": P }, "payload": [ { ...company... } ] } ``` ### 响应envelope差异: | 属性 | Chatwoot | GoChat | 状态 | |---|---|---|---| | 顶层key | payload (数组) / payload (单对象) | data | ⚠️ GoChat用统一`data`而非`payload` | | total_count | ✅ | ✅ (total_count in meta) | ✅ 兼容 | | page | ✅ | ✅ | ✅ 兼容 | | success字段 | ❌ (Chatwoot无) | ✅ | ⚠️ GoChat额外字段 | | per_page | ❌ (Chatwoot不输出) | ✅ | ⚠️ GoChat额外字段 | **说明**: GoChat现在使用统一的APIResponse{success, data, meta} envelope。与Chatwoot的`payload` key不同,但使用`total_count`而非`count`。这是一个系统性设计选择(全项目统一),而非Companies模块独有。 ### Company单对象字段对比: | 字段 | Chatwoot | GoChat | 状态 | |---|---|---|---| | id | ✅ (integer) | ✅ (integer) | ✅ 兼容 | | name | ✅ | ✅ | ✅ 兼容 | | domain | ✅ | ✅ | ✅ 兼容 | | description | ✅ | ✅ | ✅ 兼容 | | custom_attributes | ✅ (jsonb) | ✅ (jsonb) | ✅ 兼容 | | contacts_count | ✅ (counter cache列) | ❌ **缺失** | ❌ | | avatar_url | ✅ | ❌ **缺失** | ❌ | | additional_attributes | ✅ (schema有但view不渲染) | ❌ **缺失字段** | ⚠️ | | account_id | ❌ (Chatwoot不输出) | ✅ | ⚠️ GoChat额外暴露 | | website_url | ❌ (Chatwoot无此字段) | ✅ | ⚠️ GoChat额外字段 | | favicon_url | ❌ (Chatwoot用avatar_url) | ✅ | ⚠️ GoChat额外字段 | | last_activity_at | ✅ (Unix timestamp integer) | ⚠️ (RFC3339 string) | ❌ **格式不兼容** | | created_at | ✅ (Unix timestamp integer) | ⚠️ (RFC3339 string) | ❌ **格式不兼容** | | updated_at | ✅ (Unix timestamp integer) | ⚠️ (RFC3339 string) | ❌ **格式不兼容** | ### 嵌套资源envelope: | 资源 | Chatwoot envelope | GoChat envelope | 状态 | |---|---|---|---| | contacts | payload[] + meta{total_count} | data[] + meta{total_count,page,per_page} | ⚠️ key不同 | | conversations | payload[] (无分页meta) | data[] + meta{total_count,page,per_page} | ⚠️ key不同; GoChat多分页 | | notes | payload[] (无分页meta) | data[] + meta{total_count,page,per_page} | ⚠️ key不同; GoChat多分页 | **结论: 响应格式 ≈60%一致 — envelope key(data vs payload)、时间戳格式(RFC3339 vs Unix)、缺失字段(contacts_count/avatar_url)是三大不兼容项** --- ## 4. 错误处理与状态码对比 | 场景 | Chatwoot | GoChat | 状态 | |---|---|---|---| | Create成功 | 200 (implicit) | 201 | ⚠️ GoChat用201更符合REST,但与Chatwoot不一致 | | Create验证失败 | — (Rails异常) | 400 (VALIDATION_ERROR) | ⚠️ 行为差异 | | Update成功 | 200 | 200 | ✅ 兼容 | | Delete成功 | 200 空body | 204 No Content | ⚠️ Chatwoot返回200空body, GoChat返回204 | | Search空query | 422 {error:...} | 回退到list (200) | ❌ **不兼容** | | Company不存在 | 404 | 404 (handleServiceError now detects "not found") | ✅ **已修复** | | 权限不足 | 403 (Pundit) | — | ❌ **缺失** — GoChat无权限检查 | | Companies未启用 | 403 (feature flag) | — | ❌ **缺失** | **结论: 错误处理 ≈50%一致 — 404已修复是好消息, 但Search空query、权限检查、Delete状态码仍不兼容** --- ## 5. 业务逻辑对比 | 功能 | Chatwoot | GoChat | 状态 | |---|---|---|---| | 排序(Sift) | ✅ name/domain/created_at/last_activity_at/contacts_count | ✅ name/domain/created_at/last_activity_at | ⚠️ 缺contacts_count排序 | | 分页 | ✅ 固定25/page | ✅ 动态per_page | ⚠️ 默认值不同 | | 搜索ILIKE | ✅ name/domain | ✅ name/domain/description | ⚠️ GoChat多搜description | | Contacts搜索 | ✅ name/email/phone/identifier | ❌ **缺失端点** | ❌ | | Conversations分页 | ❌ (限20, 无分页) | ✅ (有分页) | ⚠️ 行为差异 | | Notes分页 | ❌ (限20, 无分页) | ✅ (有分页) | ⚠️ 行为差异 | | Domain唯一性 | ✅ (scoped to account_id) | ❌ **缺失验证** | ❌ | | Domain格式验证 | ✅ (regex) | ❌ **缺失** | ❌ | | Custom attributes更新 | ✅ merge逻辑 | ❌ 全量替换 | ❌ **行为不兼容** | | Avatar处理 | ✅ (ActiveStorage + purge) | ❌ **缺失整个avatar功能** | ❌ | | Favicon自动获取 | ✅ (AvatarFromFaviconJob) | ❌ **缺失** | ⚠️ | | Contact关联模式 | ✅ 1:N (company_id FK) | ⚠️ M:N (company_contacts关联表) | ❌ **架构不兼容** | | Last activity rollup | ✅ (5min interval) | ❌ **缺失** | ❌ | | Contacts count cache | ✅ (contacts_count列) | ❌ **缺失** | ❌ | **结论: 业务逻辑 ≈40%一致 — Contact关联架构、custom_attributes merge、domain验证、avatar、last activity是关键缺失** --- ## 6. 与旧报告(首次验收)对比 — 已修复项 | 项 | 旧报告状态 | 当前状态 | 变化 | |---|---|---|---| | AddContact端点 | ❌ 缺失 | ✅ 已实现 (POST /:company_id/contacts/:contact_id) | ✅ **修复** | | RemoveContact端点 | ❌ 缺失 | ✅ 已实现 (DELETE /:company_id/contacts/:contact_id) | ✅ **修复** | | Company不存在返回500 | ❌ 返回500 | ✅ 返回404 (handleServiceError检测"not found") | ✅ **修复** | | 响应envelope key | ❌ companies/contacts/notes/conversations | ✅ 统一data (APIResponse) | ⚠️ **改善了结构性,但key名仍是data而非payload** | | total_count key | ❌ count | ✅ total_count (APIResponse.MetaBody) | ✅ **修复** | | Delete返回200+JSON | ❌ 200 + {"message":"company deleted"} | ✅ 204 No Content | ⚠️ **改善了但Chatwoot是200空body而非204** | | 排序支持 | ❌ 固定created_at DESC | ✅ name/domain/created_at/last_activity_at | ✅ **修复** | --- ## 7. 验收结论 **验收结果: ❌ 不合格** 5项验收标准评分: 1. **接口路径一致**: ≈80% — 缺3个端点, 1个路径差异 2. **请求参数一致**: ≈75% — 缺avatar/additional_attributes, custom_attributes行为不同 3. **响应格式一致**: ≈60% — envelope key(data vs payload)、时间戳格式(RFC3339 vs Unix)、缺失字段 4. **错误处理与状态码一致**: ≈50% — Search空query、权限检查、Delete状态码 5. **业务逻辑一致**: ≈40% — Contact关联架构、custom_attributes merge、domain验证 **综合一致度: ≈61% (5项加权平均)** ### 高优先级不兼容项(影响客户端集成): 1. **响应envelope key** — `data` vs `payload` (全项目统一设计,改动影响全局) 2. **时间戳格式** — RFC3339 string vs Unix integer (created_at/updated_at/last_activity_at) 3. **Contact关联架构** — M:N vs Chatwoot的1:N (数据模型差异,影响所有嵌套查询) 4. **Custom attributes更新** — 全量替换 vs merge (破坏渐进更新语义) 5. **Search空query行为** — 回退到list vs 422 (客户端依赖422做验证) 6. **缺失contacts/search端点** — 无法搜索公司关联联系人 7. **缺失destroy_custom_attributes端点** — 无法删除指定custom_attributes键 8. **缺失avatar功能** — 无上传/删除头像 9. **缺失权限检查** — 无Pundit式权限控制 ### 中优先级缺失: 10. Domain唯一性/格式验证缺失 11. contacts_count counter cache缺失 12. last_activity_at rollup缺失 13. additional_attributes字段缺失 ### 低优先级差异(可容忍): 14. 分页默认值不同(25 vs 动态) 15. Conversations/Notes分页(Chatwoot限20 vs GoChat分页) 16. GoChat额外字段(website_url/favicon_url/account_id) 17. Delete返回204而非200空body 18. Create返回201而非200 **建议下一步**: - 优先修复(影响客户端): 时间戳格式、envelope key、custom_attributes merge、Search空query行为 - 其次修复(功能完善): contacts/search端点、destroy_custom_attributes、domain验证、avatar功能 - 架构决策: Contact关联模式是否统一到Chatwoot的1:N (需要CTO评估) - 全项目层面: envelope key(data vs payload)是统一设计,需要与CTO确认是否全局切换