Files
hfb_sys/docs/群聊优化-发布群与企业微信群二维码.md
T
ymlandClaude Opus 4.8 423c142967 提交1: 数据层改造 - 发布群与企业微信群二维码
- 新增数据库迁移 000008: chat_conversations 增加 listing_id, 新建 chat_qrcode_pool 表
- 更新 GORM 模型: ChatConversation 新增 ListingID 字段, 新增 ChatQrCode 模型
- 实现二维码池管理后端 API: 创建/列表/统计/更新/删除
- 新增管理后台路由: /api/admin/chats/qrcodes
- 新增系统配置: chat.listing_group_welcome, chat.qrcode_low_stock_threshold
- 更新优化方案文档: 废弃双群方案, 采用单群方案

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:57:10 +08:00

577 lines
28 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.
# 群聊优化方案:发布群 + 企业微信群二维码(单群方案)
> 日期:2026-06-17
> 状态:方案定稿,采用单群方案,待实施
> 更新:2026-06-17 废弃双群设计,采用单群方案
## 核心决策:采用单群方案
**废弃双群设计,只保留发布群**,理由:
1. **业务约束决定**:一个账号同时只有一个活跃订单(`InTransaction` 锁),订单群的"私密隔离"价值为零
2. **开发成本**:单群方案工作量降低 40%,代码更简洁
3. **运维成本**:会话数减半,SSE 推送压力减半
4. **用户体验**:一个群承载全生命周期沟通,避免两个群的困惑
5. **隐私保障**:通过消息可见性过滤实现租客隔离(租客只看加入后的消息)
## 已确认决策
| # | 决策点 | 结论 |
|---|--------|------|
| 1 | 群聊架构 | **单群方案**:发布群承载全生命周期沟通(咨询 + 交接 + 售后) |
| 2 | 二维码管理权限 | 复用现有 `cs` 客服角色(不细分权限) |
| 3 | 企业微信群二维码 7 天失效 | 客服手动维护 + `expires_at` 自动判断辅助 |
| 4 | 租客移出时机 | 订单终态后**立即移出**(售后窗口在订单状态中体现) |
| 5 | 存量账号历史数据 | **不做补建**,仅对新发布的账号生效 |
| 6 | 发布群创建时机 | **发布提交即建群**(不等审核),前端自动跳进群 |
| 7 | 消息历史隐私 | 租客只能看到**加入时间之后**的消息(通过 `participant.joined_at` 过滤) |
---
## 一、背景与问题
### 1.1 业务痛点
当前群聊只在**订单付款成功后**才自动创建(订单群),存在一个问题:
> 租客下单后需要联系号主(卖家)交接账号时,号主可能不在线、没及时看到站内消息,导致租客**联系不到卖家**。
号主不常驻站内 IM,但「企业微信」是其日常高频使用的沟通工具。
### 1.2 优化目标
通过两个机制解决「联系不到卖家」:
1. **发布即建群**:账号发布(上架)后立即建立「发布群」,号主常驻其中,让潜在租客在任何阶段都能找到号主。
2. **企业微信群引流**:建群后自动发送一段欢迎语 + 一张企业微信群二维码,引导号主扫码进入客服预备好的企业微信群——客服通过企业微信能**实时触达**号主,大幅提升响应率与进群概率。
---
## 二、现状分析(As-Is
### 2.1 系统栈
Go (Gin + GORM + MySQL + Redis) 后端 + Vue3 前端。聊天为**完全自建的站内 IM**,无第三方 IM 服务集成,基于 MySQL 持久化 + 进程内 SSE 实时推送(`chathub` 包)。
### 2.2 群聊数据模型
| 表 | 模型 | 作用 |
|---|---|---|
| `chat_conversations` | `ChatConversation` | 会话主表。`order_id` 唯一索引(一订单一会话),`type` 默认 `order_group`,可选 `general_support`;状态 `active/archived/closed` |
| `chat_participants` | `ChatParticipant` | 成员表。`participant_type`=`user/admin``role`=`owner/renter/support`;唯一键 `(conversation_id, participant_type, participant_id)`**含 `joined_at` 字段(关键!)** |
| `chat_messages` | `ChatMessage` | 消息表。`sender_type`=`user/admin/system``content_type`=`text/image/file/system``attachment_urls` 是 JSON 数组 |
| `chat_quick_replies` | — | 客服快捷回复模板 |
关键事实:
- **没有「群」表**"群"就是 `type='order_group'` 的 conversation
- **群聊/账号维度没有任何二维码字段**
- **`participant.joined_at` 字段已存在**,可直接用于消息可见性过滤
### 2.3 群聊创建与触发链(核心)
**唯一自动建群入口**:订单支付成功 → 同事务内建群。
```
支付渠道回调/查询确认 paid
→ payment.confirmPaid (channel_status.go:46)
→ order.ConfirmPaidFromChannelTx (lifecycle.go:159) // 单事务
├── 订单 pending_payment → pending_handoff
├── listing/account → rented
├── chat.EnsureOrderConversation(tx, order) // ★ 建订单群 (lifecycle.go:185)
│ - 幂等:按 order_id 查重
│ - 建 type=order_group 会话
│ - 写 3 个参与者:租客(renter) + 号主(owner) + 客服(support)
│ - 写 1 条系统欢迎消息 (chat.auto_welcome_message)
└── notification.Append (号主/租客各 1 条站内信)
→ 事务外:orderRepo.NotifyNewConversation → SSE 推送
```
另有用户主动触发的「平台客服」咨询会话(`type=general_support`),与本次优化无关。
### 2.4 业务约束:一个账号同时只有一个订单
**关键发现**:通过 `listing.InTransaction` 字段实现互斥锁。
```go
// order/assets.go:45
func reserveListingForOrder(listing *model.RentalListing) {
listing.InTransaction = true // 锁定账号,防止并发下单
}
```
**结论**:一个发布(listing)在同一时间只能有一个活跃订单,不存在"多个租客同时在群内"的场景。
### 2.5 现状关键结论
| 问题 | 结论 |
|---|---|
| 群聊何时创建 | **仅在订单支付成功时**,一订单一群。无「发布即建群」逻辑 |
| 群聊命名 | `"订单群聊 " + 订单号`,不含账号信息 |
| 成员管理能力 | **极弱**:创建时一次性写死参与者;仅有客服转接(删旧客服加新客服),**无中途拉人/踢人接口** |
| 群聊与发布/账号关联 | **无任何关联**,只挂 `order_id` |
| 消息能力 | 自建站内 IM(文本/图片/文件附件,附件走 MinIO 文件服务) |
| 二维码能力 | 平台有「文件上传到 MinIO + 存 key」能力(收款码、支付码);但**群聊/账号维度无二维码字段** |
| 客服角色 | `roles.code='cs'`,建群时按「当前会话负载最少 + 在线状态」选最空闲客服 (`participant.go:298`) |
| 事件/队列机制 | **无通用事件总线/MQ**。副作用靠同步方法调用;异步靠轮询 Job(订单超时、退款重试) |
### 2.6 关键文件路径
| 文件 | 行 | 作用 |
|---|---|---|
| `backend/internal/model/chat.go` | 9-57 | 聊天数据模型 |
| `backend/migrations/000001_init.sql` | 456-514 | 聊天表结构 |
| `backend/internal/modules/chat/repository.go` | 24-100 | `EnsureOrderConversation` 建群逻辑 |
| `backend/internal/modules/chat/participant.go` | 298-369 | 客服选择算法 |
| `backend/internal/modules/chat/message.go` | 52-161 | 发消息 + SSE 推送 |
| `backend/internal/modules/chathub/hub.go` | — | SSE Hub(实时推送,channel 满丢事件,非可靠投递) |
| `backend/internal/modules/order/lifecycle.go` | 159-219 | 付款成功事务,内含建群调用 (185) |
| `backend/internal/modules/order/assets.go` | 45 | `InTransaction` 锁逻辑 |
| `backend/internal/modules/payment/channel_status.go` | 46-87 | 支付确认入口 |
| `backend/internal/modules/listing/review.go` | 90-127 | 审核通过(上架)触发点 |
| `backend/internal/modules/listing/mutation.go` | 16-66 | 发布创建(含免审直发分支) |
| `backend/internal/modules/file/*` | — | MinIO 文件上传服务 |
| `backend/internal/router/router.go` | 162, 440-452, 531-547 | chat 依赖装配、用户端/管理端聊天路由 |
---
## 三、目标设计(To-Be):单群方案
### 3.1 核心设计
**一个发布 = 一个发布群**,承载全生命周期沟通:
```
发布提交 → 建发布群(listing_group
├─ 固定成员:号主 + 客服
├─ 自动消息:欢迎语 + 企业微信群二维码图片
└─ 前端跳转:发布成功后自动进群,号主立即看到二维码
租客付款 → 拉租客进发布群
├─ 幂等加入(已在群内则跳过)
├─ 发系统消息"租客 xxx 已付款(订单 xxx"
└─ 租客**只能看到加入时间之后的消息**(消息可见性过滤)
订单交接 → 在同一个群内沟通
├─ 号主提交交接说明
├─ 租客确认交接
└─ 客服介入处理纠纷
订单终态 → 移出租客
├─ 删除租客的 chat_participants 行
├─ 发系统消息"订单已结束,租客已退出群聊"
└─ 号主 + 客服常驻群内
下次出租 → 新租客付款后加入同一个群
└─ 看不到之前租客的消息(通过 joined_at 过滤)
```
### 3.2 发布群职责
| 职责 | 说明 |
|------|------|
| **咨询阶段** | 潜在租客可通过账号详情页进群咨询(未付款前只读) |
| **交接阶段** | 租客付款后加入群,在群内完成账号交接 |
| **售后阶段** | 租期内问题在群内沟通,客服介入处理 |
| **长期入口** | 号主常驻群内,企业微信二维码持续有效,提升触达率 |
### 3.3 消息历史隐私保障(核心)
**问题**:新租客加入时,会看到之前租客的聊天记录吗?
**解决**:通过 `participant.joined_at` 过滤消息可见性。
```go
// 租客查询消息时的过滤逻辑
func (r *Repository) ListMessages(conversationID, userID uint64, userType string) {
var participant ChatParticipant
db.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?",
conversationID, userType, userID).First(&participant)
query := db.Where("conversation_id = ?", conversationID)
// 租客只能看到加入时间之后的消息
if userType == "user" && participant.Role == "renter" {
query = query.Where("created_at >= ?", participant.JoinedAt)
}
// 号主和客服看全部消息(便于连续服务)
query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&messages)
}
```
**效果**
- 租客 A2026-01-01 加入,看到 2026-01-01 ~ 2026-01-15 的消息
- 租客 A 订单终态后移出
- 租客 B2026-02-01 加入,只看到 2026-02-01 之后的消息,看不到租客 A 的对话
- 号主/客服:看到全部消息,便于了解历史情况
### 3.4 企业微信群二维码的作用链
```
客服在后台预先上传一批企业微信群二维码到「二维码池」
号主发布账号 → 发布提交时自动建发布群
从二维码池取一张 unused 二维码,标记 used + 记录 conversation_id
在发布群内发送:
① 系统欢迎语(文本)
② 二维码图片消息(image)+ 引导文案"👇 请扫码加入企业微信群,方便客服与您及时联系"
前端发布成功后自动跳进发布群,号主第一眼看到欢迎语和二维码
号主扫码进企业微信群
客服通过企业微信能实时联系到号主(突破站内消息到达率低的限制)
```
---
## 四、详细设计
### 4.1 数据层改动
#### 4.1.1 `chat_conversations` 表新增 `listing_id` 列
```sql
ALTER TABLE chat_conversations
ADD COLUMN listing_id BIGINT UNSIGNED NULL,
ADD INDEX idx_chat_conversations_listing (listing_id);
```
**设计决策**
- 保留现有 `order_id` 字段,兼容历史订单群(不做迁移)
- `listing_id` 用**普通索引**(非唯一):用幂等逻辑(建群前查重)保证一发布一群,比唯一约束更灵活
- GORM 模型 `ChatConversation` 增加 `ListingID *uint64` 字段
- 新增会话类型常量 `listing_group`
#### 4.1.2 新表 `chat_qrcode_pool`(企业微信群二维码池)
```sql
CREATE TABLE chat_qrcode_pool (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
image_url VARCHAR(512) NOT NULL, -- MinIO 存储的二维码图片 URL
status VARCHAR(16) NOT NULL DEFAULT 'unused', -- unused / used / disabled
conversation_id BIGINT UNSIGNED NULL, -- 发出后记录到哪个发布群(留痕)
used_at DATETIME NULL,
expires_at DATETIME NULL, -- 企微群码失效时间(默认创建+7天)
created_by BIGINT UNSIGNED NOT NULL, -- 上传的客服 admin_id
note VARCHAR(255) NOT NULL DEFAULT '', -- 备注,如"7月企微群A"
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_qrcode_status (status),
INDEX idx_qrcode_expires (expires_at)
);
```
**字段说明**
- **status 流转**`unused`(待用)→ `used`(已发放,绑定到某发布群);客服可手动置 `disabled`(停用,不再发放)
- **expires_at 自动失效辅助**:客服上传时可不填,默认 `created_at + 7 天`(企微群码原生 7 天有效期)。分配时跳过 `expires_at < now()``unused` 码,自动老化
- **分配策略:一次性消耗**。每个发布群消耗一张 `unused` 且未过期的二维码,发后标记 `used` 并记录 `conversation_id`。防滥用、留痕清晰
- GORM 模型 `ChatQrCode`
#### 4.1.3 `system_configs` 新增配置项
| key | 默认值 | 作用 |
|-----|--------|------|
| `chat.listing_group_welcome` | `"欢迎加入账号群!请号主扫描下方二维码加入企业微信群,方便客服与您及时联系。"` | 发布群欢迎语 |
| `chat.qrcode_low_stock_threshold` | `5` | 二维码库存预警阈值 |
### 4.2 后端逻辑改动
#### 4.2.1 发布群创建(chat 模块新增 `listing_group.go`
新函数 `EnsureListingConversation(tx *gorm.DB, listing model.RentalListing) (conversationID uint64, error)`,对照现有 `EnsureOrderConversation` 编写:
**核心逻辑**
1. **幂等查重**:按 `listing_id``chat_conversations`,已存在则直接返回 conversation_id
2. **建会话**`ChatConversation{ListingID: listing.ID, Type: "listing_group", Title: "账号群 " + listing.ListingNo}`
3. **写参与者**
- 号主(`participant_type=user, role=owner`
- 客服(`participant_type=admin, role=support`,复用 `defaultSupportAdminID(tx)`
4. **取二维码**
```sql
SELECT * FROM chat_qrcode_pool
WHERE status='unused'
AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY id LIMIT 1 FOR UPDATE
```
取到则标记 `used` + 写 `conversation_id` + `used_at`
5. **发欢迎语**
- 一条文本系统消息(读 `chat.listing_group_welcome`
- 若取到二维码:一条图片系统消息(`content_type=image`, `attachment_urls=[image_url]`),附引导文案"👇 请扫码加入企业微信群"
- 若二维码池为空:不阻塞建群,发一条提示"客服企业微信群二维码补充中,请稍后在群内关注"
6. **更新会话 last_message**(复用现有逻辑)
7. **库存预警**:发放后若剩余 `unused` 数量 ≤ 阈值,给所有 `cs` 角色客服发站内信"企业微信群二维码库存不足(剩 N 张),请及时补充"
#### 4.2.2 发布触发点(listing/mutation.go 的 `Create`
**触发时机改为「发布提交时」**(不等审核)。在 `Create` 事务内,`tx.Create(&listing)` 之后、事务 return 之前,调用建群。
**模块解耦**(仿照现有 `OrderChatNotifier` 模式):
```go
// listing/repository.go 新增接口
type ListingChatCreator interface {
EnsureListingConversation(tx *gorm.DB, listing model.RentalListing) (conversationID uint64, err error)
}
```
在 `router.go` 装配处(`chatRepo` 已在 router.go:162 就绪)注入给 listing repo。`Create` 事务内通过 `conversationID, err := r.chatCreator.EnsureListingConversation(tx, listing)` 调用,把返回的 `conversation_id` 带回 DTO。
**DTO 增强**`ListingDTO` 新增 `listing_group_conversation_id` 字段,发布接口返回时带上刚建的发布群 ID。
**事务回滚保障**:建群失败会导致整个发布事务回滚,保证数据一致性。
#### 4.2.3 前端发布成功自动跳群
这是提升进群概率的关键交互。当前发布成功逻辑在 `frontend/src/features/seller/composables/usePublishForm.ts:189`,走 `router.push(options.submitSuccessPath)`。
**改造点**
1. 发布接口返回的 `ListingDTO` 现在带 `listing_group_conversation_id`
2. 发布成功后,若该字段 > 0,则 `router.push` 改为跳转到该发布群聊天页(如 `/chats/:conversationId`),而不是原来的发布成功列表页
3. 跳进群后,号主第一眼就看到系统欢迎语 + 二维码图片,自然引导扫码进企业微信群
**注意**:审核中状态的账号虽已建群,但此时群内只有号主+客服,无租客。审核通过上架后租客才能下单被拉进。群聊本身与审核解耦。
#### 4.2.4 付款后拉租客进发布群(order/lifecycle.go
在 `ConfirmPaidFromChannelTx` 现有建订单群逻辑**之后**(或直接替换),新增/修改为:
```go
// 废弃原 chat.EnsureOrderConversation(tx, *order) - 不再建订单群
// 改为拉租客进发布群
if err := chat.AddRenterToListingConversation(tx, listing.ID, order.RenterID, order.OrderNo); err != nil {
// 优雅降级:历史发布无发布群时,err 返回 nil,不阻塞付款流程
return 0, err
}
```
**新函数 `AddRenterToListingConversation(tx, listingID, renterID, orderNo)`**
1. 按 `listing_id` 查发布群。**找不到则跳过**(优雅降级:历史发布无发布群,不影响下单)
2. 幂等插入参与者 `(conversation_id, 'user', renterID, role='renter', joined_at=NOW())``OnConflict{DoNothing}`
3. 发系统消息"租客 xxx 已加入群聊(订单 <orderNo>"
4. SSE 通知在事务外统一刷新,无需在此处理
**关键**`joined_at` 字段记录租客加入时间,用于后续消息可见性过滤。
#### 4.2.5 租客移出发布群(订单终态)
订单进入终态时,把租客移出发布群,保持群成员恒定为「号主 + 客服」,避免历史租客残留导致隐私泄露。
**新函数 `RemoveRenterFromListingConversation(tx, listingID, renterID, reason)`**
1. 按 `listing_id` 查发布群,找不到则跳过
2. 删除该租客的 `chat_participants` 行:
```go
tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ? AND role = ?",
conversationID, "user", renterID, "renter").Delete(&model.ChatParticipant{})
```
3. 发系统消息(如"订单已结束,租客已退出群聊")
**触发点**(在订单终态流转处调用):
| 订单事件 | 文件 | 动作 |
|---|---|---|
| 订单完成 | `order/checkout_finalize.go` | 移出租客 |
| 租客取消 | `order/lifecycle.go` 的 `Cancel` | 移出租客 |
| 退款终结 / 关闭 | `order/refund.go` / `closed` 流转 | 移出租客 |
**注意**`InTransaction` 锁保证一个账号同时只有一个活跃订单,故"同时多租客共群"不会发生,移出机制主要清理历史租客。
#### 4.2.6 消息查询增加可见性过滤(chat/message.go
修改现有消息列表查询接口,增加租客可见性过滤:
```go
func (r *Repository) ListMessages(ctx context.Context, conversationID uint64, userID uint64, userType string, offset, limit int) {
// 查询当前用户的参与者信息
var participant model.ChatParticipant
err := r.db.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?",
conversationID, userType, userID).First(&participant).Error
if err != nil {
return nil, err // 不在群内,无权查看
}
query := r.db.Where("conversation_id = ?", conversationID)
// 租客只能看到加入时间之后的消息
if userType == "user" && participant.Role == "renter" {
query = query.Where("created_at >= ?", participant.JoinedAt)
}
// 号主和客服看全部消息
var messages []model.ChatMessage
query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&messages)
return messages, nil
}
```
**效果**
- 新租客加入时,只看到自己加入后的消息
- 之前租客的聊天记录完全不可见
- 号主和客服看全部消息,便于连续服务
#### 4.2.7 客服后台二维码池管理(chat 模块新增 `qrcode.go` + 路由)
挂在 `router.go:531` 现有客服路由组下:
| Method | Path | 作用 |
|--------|------|------|
| `POST` | `/api/admin/chats/qrcodes` | 客服上传二维码(先经 file 模块上传 MinIO 拿 URL,再写入池) |
| `GET` | `/api/admin/chats/qrcodes` | 列表(支持按 status 过滤、分页) |
| `PATCH` | `/api/admin/chats/qrcodes/:id` | 编辑备注 / 置 `disabled` / 更新 `expires_at` |
| `DELETE` | `/api/admin/chats/qrcodes/:id` | 删除(仅 `unused` 可删,`used` 保留留痕) |
| `GET` | `/api/admin/chats/qrcodes/stats` | 库存统计(unused / used / disabled 计数) |
权限:`cs` 角色下(复用现有 `chat:manage` 或新增 `chat:qrcode` 权限)。
### 4.3 前端改动
#### 4.3.1 客服后台:二维码池管理页
- 新页面 `frontend/src/features/admin/chat/QrCodePool.vue`:列表 + 上传 + 状态筛选 + 库存统计卡片
- 上传组件复用现有文件上传逻辑(项目已有支付/收款码上传 UI 可参考)
- 表格显示:图片缩略图、状态、绑定会话、过期时间、备注、操作按钮
#### 4.3.2 用户端:会话列表区分群类型
- 发布群走现有 `/api/chats` 接口(已返回 `type` 字段),前端按 `type` 加图标/前缀区分:
- 发布群:`📢 账号群 <listing_no>`
- 历史订单群:`📦 订单 <order_no>`(兼容存量数据)
- 发布群内展示欢迎语 + 二维码图片消息(`content_type=image` 已支持)
#### 4.3.3 发布成功跳转逻辑
修改 `frontend/src/features/seller/composables/usePublishForm.ts`
```typescript
// 发布成功后
if (response.data.listing_group_conversation_id) {
// 跳转到发布群
router.push(`/chats/${response.data.listing_group_conversation_id}`)
} else {
// 兼容旧逻辑或建群失败场景
router.push(options.submitSuccessPath)
}
```
---
## 五、边界与风控
| 场景 | 处理 |
|------|------|
| **历史数据**(上线前已发布的账号) | 无发布群。租客付款时 `AddRenterToListingConversation` 找不到群则跳过,不影响下单。**不做后台批量补建**,仅新发布生效 |
| **历史订单群**(上线前已建的订单群) | 保留原样,`type=order_group` + `order_id`。新老群并行,逐步过渡到发布群 |
| **一号主发多个账号** | 每个发布独立建群,号主进多个发布群。每个群各消耗一张二维码 |
| **同一账号被租多次** | 幂等返回同一发布群;靠「订单终态移出租客」保持群成员干净,规避历史租客共群隐私泄露 |
| **下线后重新发布** | 同 listing_id 重新上架:幂等返回已有群,不重建。发全新账号(新 listing_id):新建发布群 |
| **二维码用尽** | 建群不阻塞,发提示文案;同时预警站内信给客服 |
| **企业微信群二维码 7 天失效** | 企业微信原生群码有有效期。**运营层面靠客服定期更换**(disabled 旧码、传新码)。系统辅助:`expires_at` 字段在分配时跳过过期码 |
| **租客退出/订单结束** | 订单终态立即移出租客;发布群作为联系入口保留(号主+客服常驻) |
| **客服被拉进海量群** | 现有客服工作台已按会话负载分配,规模上来后需评估 filter/降噪(现状 `support.go` 已有列表 filter 能力) |
| **租客恶意查看历史消息** | 通过 `participant.joined_at` 过滤,后端强制校验,租客无法绕过 |
---
## 六、实施计划(分 3 个提交)
| 提交 | 内容 | 可独立验收 | 预计工作量 |
|------|------|-----------|-----------|
| **提交 1** | 数据层(migration + 模型 + 配置)+ 二维码池后台 CRUD(API + 前端管理页) | ✅ 不碰现有聊天逻辑 | 2 天 |
| **提交 2** | 发布群核心:`EnsureListingConversation` + listing 触发点 + 付款拉租客 + 订单终态移出租客 + 消息可见性过滤 | ✅ 改造现有聊天流 | 3 天 |
| **提交 3** | 体验完善:库存预警站内信、前端发布跳群、前端群类型图标区分 | ✅ 增量优化 | 2 天 |
**总工作量**:约 7 个工作日(vs 双群方案 10+ 天)
---
## 七、单群方案优势总结
### 7.1 vs 双群方案对比
| 维度 | 双群方案 | 单群方案(采用) | 优势 |
|------|---------|--------------|------|
| **开发复杂度** | 两套建群逻辑 + 两套成员管理 | 一套建群逻辑 + 简单成员进出 | 代码量减少 40% |
| **运维成本** | 会话数翻倍,SSE 推送压力大 | 会话数减半 | 资源消耗减少 50% |
| **用户体验** | 两个群聊天,不知道在哪说 | 一个群承载全生命周期 | 更直观清晰 |
| **隐私保障** | 订单群独立隔离 | 消息可见性过滤(`joined_at` | 同样安全 |
| **数据库变更** | 加 `listing_id`,保留 `order_id` | 加 `listing_id`,兼容 `order_id` | 同等复杂度 |
| **业务契合度** | 过度设计(同时只有一个租客) | 契合业务约束 | 避免过度工程 |
### 7.2 核心技术亮点
1. **消息可见性过滤** - 通过 `participant.joined_at` 实现租客隔离,简单可靠
2. **优雅降级** - 历史数据无发布群时跳过,不阻塞下单流程
3. **模块解耦** - 通过接口注入避免 listing 直接依赖 chat
4. **幂等设计** - 重复调用建群/拉人接口安全无副作用
5. **事务一致性** - 建群失败回滚整个发布事务,保证数据正确性
### 7.3 风险与缓解
| 风险 | 缓解措施 |
|------|---------|
| 消息可见性过滤失效 | 后端强制校验 `joined_at`,前端无法绕过;增加单元测试覆盖 |
| 历史订单群兼容性 | 保留 `order_id` 字段,新老群并行,不做强制迁移 |
| 二维码库存耗尽 | 库存预警 + 优雅降级(建群不阻塞) |
| 客服工作量激增 | 监控客服平均会话数,必要时调整分配算法 |
---
## 八、未来扩展
### 8.1 可选增强(本期不做)
1. **二维码自动老化 Job** - 定时标记过期码为 `disabled`,减轻客服负担
2. **订单终态统一入口** - 抽象 `finalizeOrder(reason)` 避免移出逻辑遗漏
3. **客服会话过滤** - 提前规划发布群数量激增后的工具支持
4. **AB 测试灰度** - 先对部分号主开启,验证企业微信引流效果
### 8.2 长期关注点
1. **SSE 推送可靠性** - 如果发布群量大,考虑引入 MQ
2. **企业微信 API 对接** - 后期可研究是否有群码自动创建 API
3. **消息分页性能** - 发布群长期存在,消息量可能很大,需优化查询性能
---
## 九、验收标准
### 9.1 功能验收
- [ ] 发布账号时自动建发布群,群内有号主、客服、欢迎语、二维码图片
- [ ] 前端发布成功后自动跳进发布群
- [ ] 租客付款后自动加入发布群,发系统消息
- [ ] 租客只能看到加入时间之后的消息(历史消息不可见)
- [ ] 订单终态后租客自动移出群,发系统消息
- [ ] 同一账号多次出租,新租客看不到之前租客的对话
- [ ] 客服可在后台上传/管理二维码,查看库存统计
- [ ] 二维码库存不足时,客服收到站内信预警
### 9.2 性能验收
- [ ] 发布群创建耗时 < 500ms(含二维码分配)
- [ ] 消息查询带可见性过滤,耗时无明显增加(< 100ms)
- [ ] 历史数据无发布群时,付款流程不受影响
### 9.3 兼容性验收
- [ ] 历史订单群正常展示和使用
- [ ] 新老群类型在前端有明确区分
- [ ] 二维码用尽时建群不阻塞,发提示文案
---
## 十、总结
单群方案通过**消息可见性过滤**实现了双群方案的隐私隔离效果,同时大幅降低开发和运维成本。方案契合业务约束(一个账号同时只有一个订单),避免了过度设计,是更优的技术选择。
**核心价值**
- ✅ 解决「联系不到卖家」痛点(发布即建群 + 企业微信引流)
- ✅ 降低开发成本 40%,运维成本 50%
- ✅ 提升用户体验(一个群承载全生命周期)
- ✅ 保障隐私安全(消息可见性过滤)
**建议下一步**:开始实施提交 1(数据层 + 二维码池管理),为核心功能打好基础。