feat(shangwutong): sync classifications and update conversations

This commit is contained in:
2026-09-11 17:08:18 +08:00
parent a06d4f4199
commit 31f157a4f5
28 changed files with 2541 additions and 232 deletions
@@ -0,0 +1,251 @@
# 商务通分类同步与会话分类配置实施计划
> 日期:2026-09-11
> 状态:首版实现完成,待真实商务通账号灰度验证
> 关联调研:[`docs/research/2026-09-11-shangwutong-pc-classification-protocol.md`](../research/2026-09-11-shangwutong-pc-classification-protocol.md)
## 1. 目标与边界
为每个对接的商务通账号提供:
1. 从商务通读取对话分类和客户颜色分类定义;
2. 在对应收件箱提供“同步商务通分类”按钮;
3. 在商务通会话右侧客户信息中提供两个独立下拉框;
4. 修改后通过 Connector 下发到商务通;
5. GoChat 不预设、创建、编辑或删除商务通分类定义。
明确不做:
- 不把商务通分类写入 GoChat 原生标签定义表;
- 不把商务通颜色分类映射为 CRM `CategoryInFo`、`ClientLabelConfig`;
- 不为未同步的收件箱生成默认分类;
- 不在浏览器直接持有或调用商务通凭据。
## 2. 已确认的商务通协议
### 2.1 分类定义
```text
POST {serverurl}/oc/SiteSetting.aspx?sn={sn}&siteid={siteid}&act=load
```
对话分类:
```text
sidkind_share = id,txt,iconindex|id,txt,iconindex|...
```
客户颜色分类:
```text
colorkind0_share = id|id|...
colorkind1_share = name|name|...
```
保存/同步请求返回 `r=ok` 或 `r=load ok`。GoChat 只读取定义,不调用保存配置接口。
### 2.2 会话分类修改
对话分类:
```text
POST {serverurl}/oc/SetSidKind.aspx
sid, oname, siteid, kind
```
客户颜色分类:
```text
POST {serverurl}/oc/changecolor.aspx
sid, oname, siteid, c0, c1, cid
```
清除颜色分类的 `RESET` 语义暂不作为首版 UI 能力暴露。
## 3. 总体架构决定
采用“GoChat 按收件箱持久化远程分类缓存”的方案:
```text
前端收件箱同步按钮
-> GoChat 管理 API
-> 已签名的 GoChat -> Connector webhook 控制事件
-> Connector 使用账号会话读取商务通
-> Connector 回调 GoChat
-> GoChat 按 inbox_id 更新缓存
-> 前端刷新列表
```
会话修改采用现有 Connector 出站操作队列:
```text
前端下拉修改
-> GoChat 校验 inbox/conversation/sid/cid
-> 持久化 outbound operation
-> Connector 读取 operation
-> 调用 SetSidKind.aspx 或 changecolor.aspx
-> 回传结果
-> GoChat 更新会话分类确认状态
```
分类缓存按 `inbox_id` 隔离。同步失败保留旧缓存,并返回可展示的错误状态。
## 4. 分步实施清单
### 阶段 1:协议值对象与 Connector 商务通客户端
- [x] 增加对话分类、客户颜色分类值对象;
- [x] 实现 `SiteSetting.aspx` 读取和严格解析;
- [x] 实现 `SetSidKind.aspx`;
- [x] 实现 `changecolor.aspx`;
- [x] 保留已有认证、超时、响应状态和不确定结果处理;
- [x] 为分隔符、空值、重复 ID、平行数组长度不一致增加测试。
阶段验证:`cd channels/shangwutong && go test ./internal/swt`
### 阶段 2:GoChat 分类缓存与 Connector 回调
- [x] 新增按 `inbox_id` 唯一的分类缓存模型及迁移;
- [x] 保存对话分类、客户颜色分类、同步时间、同步状态和最后错误;
- [x] 增加管理员读取分类缓存 API;
- [x] 增加管理员触发同步 API;
- [x] 增加 Connector 结果回调 API,验证账号、收件箱和幂等键;
- [x] 扩展已签名 webhook 事件 `classification_sync_requested`;
- [x] Connector 完成远程读取后回调 GoChat;
- [x] 失败时不覆盖上一次成功缓存。
### 阶段 3:会话分类出站操作
- [x] 扩展 GoChat 会话分类 API;
- [x] 校验会话属于商务通收件箱;
- [x] 从会话/联系人渠道元数据取得 `sid` 和 `cid`;
- [x] 将 `set_chat_kind`、`set_customer_color` 写入现有出站队列;
- [x] Connector 出站 worker 调用对应商务通接口;
- [x] 增加操作结果回调和幂等处理;
- [x] 只有商务通确认成功后更新确认值,失败/不确定保留状态并提示。
### 阶段 4:前端收件箱设置
- [x] 在商务通收件箱设置中增加“同步商务通分类”按钮;
- [x] 展示对话分类和客户分类同步结果;
- [x] 同步中禁用按钮;
- [x] 同步失败保留旧列表;
- [x] 不提供分类新增、编辑、删除控件。
### 阶段 5:前端会话右侧客户信息
- [x] 仅商务通会话显示“商务通分类”区块;
- [x] 增加对话分类下拉;
- [x] 增加客户分类下拉;
- [x] 未同步、失效或缺少 `cid` 时安全禁用;
- [x] 提交中禁用对应控件;
- [x] 根据远程确认结果更新或恢复值(轮询会话确认属性,超时显示处理中);
- [x] 不复用 GoChat 原生标签组件。
### 阶段 6:验证与文档
- [x] Go 单元测试与全模块编译;
- [x] SQLite migration/build/test(迁移已通过最小 SQLite 外键/建表冒烟验证);
- [x] 前端 production build;
- [x] Connector 协议 mock 测试;
- [x] 更新调研文档中的实现状态;
- [x] 检查无凭据泄漏、跨 inbox 读取和跨账号分类串用。
## 5. API 草案
### 5.1 读取缓存
```http
GET /api/v1/accounts/:account_id/inboxes/:inbox_id/shangwutong/classifications
```
### 5.2 触发同步
```http
POST /api/v1/accounts/:account_id/inboxes/:inbox_id/shangwutong/classifications/sync
```
### 5.3 修改会话分类
```http
PATCH /api/v1/accounts/:account_id/conversations/:conversation_id/shangwutong-classifications
```
请求体只允许一个修改目标:
```json
{"chat_kind_id": 2}
```
或:
```json
{"customer_color_id": 1}
```
### 5.4 Connector 回调
Connector 专用回调不暴露给普通前端用户,沿用 Connector 平台鉴权和收件箱授权边界。
## 6. 数据模型草案
分类定义不是 GoChat 原生标签,建议独立表:
```text
shangwutong_classification_caches
- inbox_id unique
- conversation_kinds JSON
- customer_color_kinds JSON
- sync_status pending/succeeded/failed
- synced_at nullable
- last_error_code nullable
- last_error_message nullable
- created_at
- updated_at
```
分类值只保留商务通远程 ID、名称及图标索引;不生成 GoChat `Tag` 记录。
## 7. 验收标准
- 不同步的商务通收件箱不显示伪造分类;
- 同步成功后两个列表均可按收件箱读取;
- 同步失败不会清空上一次成功结果;
- 会话右侧只对商务通显示两个独立下拉;
- 选择对话分类最终请求 `SetSidKind.aspx` 的 `kind`;
- 选择客户分类最终请求 `changecolor.aspx` 的 `c0/c1/cid`;
- `CategoryID`、`LabelID` 不参与上述实时会话接口;
- 重复点击和重试不会产生冲突修改;
- 远程失败或不确定时 UI 不显示虚假的成功状态;
- 不同商务通收件箱的分类 ID 不互相串用。
## 8. 风险与暂缓项
1. 商务通 PC 端分类读取响应存在旧式字符串协议,必须拒绝坏格式,不应静默错配两个颜色数组;
2. `cid` 是客户级标识,客户颜色修改的跨会话作用范围需灰度账号验证;
3. Connector 到 GoChat 的同步回调必须使用幂等键,避免 webhook 重试覆盖更新状态;
4. 首版不暴露清除客户分类按钮,待 `RESET` 完整协议验证后再增加;
5. 分类定义同步和会话分类修改必须分开建模,不能以“同步成功”代替“会话修改成功”。
## 9. 首版实现记录
已落地的主要文件:
- Connector 协议:`channels/shangwutong/internal/swt/classifications.go`;
- Connector 回调客户端:`channels/shangwutong/internal/gochat/classifications.go`;
- Connector 会话操作:`channels/shangwutong/internal/delivery/outbound.go`;
- Connector webhook 分发:`channels/shangwutong/internal/httpapi/server.go`;
- GoChat 缓存模型与迁移:`backend/internal/model/channel_shangwutong_classification_cache.go`、`backend/migrations/000087_*`;
- GoChat API 与任务:`backend/internal/handler/api/v1/shangwutong_connector_handler.go`、`backend/internal/service/shangwutong_webhook_delivery.go`;
- 前端同步按钮:`frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ShangwutongConfiguration.vue`;
- 前端会话下拉:`frontend/app/javascript/dashboard/routes/dashboard/conversation/ShangwutongClassifications.vue`。
验证结果:
```text
cd channels/shangwutong && go test ./... && go vet ./...
cd backend && GOCHAT_TEST_DB=sqlite go test ./...
cd frontend && pnpm build
```
均已通过。前端修改文件的 ESLint 无错误;现有设置组件保留一条既有的动态 i18n key warning。真实商务通账号的灰度协议验证仍待安排。