- 新增数据库迁移 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>
28 KiB
群聊优化方案:发布群 + 企业微信群二维码(单群方案)
日期:2026-06-17 状态:方案定稿,采用单群方案,待实施 更新:2026-06-17 废弃双群设计,采用单群方案
核心决策:采用单群方案
废弃双群设计,只保留发布群,理由:
- 业务约束决定:一个账号同时只有一个活跃订单(
InTransaction锁),订单群的"私密隔离"价值为零 - 开发成本:单群方案工作量降低 40%,代码更简洁
- 运维成本:会话数减半,SSE 推送压力减半
- 用户体验:一个群承载全生命周期沟通,避免两个群的困惑
- 隐私保障:通过消息可见性过滤实现租客隔离(租客只看加入后的消息)
已确认决策
| # | 决策点 | 结论 |
|---|---|---|
| 1 | 群聊架构 | 单群方案:发布群承载全生命周期沟通(咨询 + 交接 + 售后) |
| 2 | 二维码管理权限 | 复用现有 cs 客服角色(不细分权限) |
| 3 | 企业微信群二维码 7 天失效 | 客服手动维护 + expires_at 自动判断辅助 |
| 4 | 租客移出时机 | 订单终态后立即移出(售后窗口在订单状态中体现) |
| 5 | 存量账号历史数据 | 不做补建,仅对新发布的账号生效 |
| 6 | 发布群创建时机 | 发布提交即建群(不等审核),前端自动跳进群 |
| 7 | 消息历史隐私 | 租客只能看到加入时间之后的消息(通过 participant.joined_at 过滤) |
一、背景与问题
1.1 业务痛点
当前群聊只在订单付款成功后才自动创建(订单群),存在一个问题:
租客下单后需要联系号主(卖家)交接账号时,号主可能不在线、没及时看到站内消息,导致租客联系不到卖家。
号主不常驻站内 IM,但「企业微信」是其日常高频使用的沟通工具。
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 字段实现互斥锁。
// 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 过滤消息可见性。
// 租客查询消息时的过滤逻辑
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)
}
效果:
- 租客 A:2026-01-01 加入,看到 2026-01-01 ~ 2026-01-15 的消息
- 租客 A 订单终态后移出
- 租客 B:2026-02-01 加入,只看到 2026-02-01 之后的消息,看不到租客 A 的对话
- 号主/客服:看到全部消息,便于了解历史情况
3.4 企业微信群二维码的作用链
客服在后台预先上传一批企业微信群二维码到「二维码池」
↓
号主发布账号 → 发布提交时自动建发布群
↓
从二维码池取一张 unused 二维码,标记 used + 记录 conversation_id
↓
在发布群内发送:
① 系统欢迎语(文本)
② 二维码图片消息(image)+ 引导文案"👇 请扫码加入企业微信群,方便客服与您及时联系"
↓
前端发布成功后自动跳进发布群,号主第一眼看到欢迎语和二维码
↓
号主扫码进企业微信群
↓
客服通过企业微信能实时联系到号主(突破站内消息到达率低的限制)
四、详细设计
4.1 数据层改动
4.1.1 chat_conversations 表新增 listing_id 列
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(企业微信群二维码池)
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 编写:
核心逻辑:
- 幂等查重:按
listing_id查chat_conversations,已存在则直接返回 conversation_id - 建会话:
ChatConversation{ListingID: listing.ID, Type: "listing_group", Title: "账号群 " + listing.ListingNo} - 写参与者:
- 号主(
participant_type=user, role=owner) - 客服(
participant_type=admin, role=support,复用defaultSupportAdminID(tx))
- 号主(
- 取二维码:
取到则标记
SELECT * FROM chat_qrcode_pool WHERE status='unused' AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY id LIMIT 1 FOR UPDATEused+ 写conversation_id+used_at - 发欢迎语:
- 一条文本系统消息(读
chat.listing_group_welcome) - 若取到二维码:一条图片系统消息(
content_type=image,attachment_urls=[image_url]),附引导文案"👇 请扫码加入企业微信群" - 若二维码池为空:不阻塞建群,发一条提示"客服企业微信群二维码补充中,请稍后在群内关注"
- 一条文本系统消息(读
- 更新会话 last_message(复用现有逻辑)
- 库存预警:发放后若剩余
unused数量 ≤ 阈值,给所有cs角色客服发站内信"企业微信群二维码库存不足(剩 N 张),请及时补充"
4.2.2 发布触发点(listing/mutation.go 的 Create)
触发时机改为「发布提交时」(不等审核)。在 Create 事务内,tx.Create(&listing) 之后、事务 return 之前,调用建群。
模块解耦(仿照现有 OrderChatNotifier 模式):
// 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)。
改造点:
- 发布接口返回的
ListingDTO现在带listing_group_conversation_id - 发布成功后,若该字段 > 0,则
router.push改为跳转到该发布群聊天页(如/chats/:conversationId),而不是原来的发布成功列表页 - 跳进群后,号主第一眼就看到系统欢迎语 + 二维码图片,自然引导扫码进企业微信群
注意:审核中状态的账号虽已建群,但此时群内只有号主+客服,无租客。审核通过上架后租客才能下单被拉进。群聊本身与审核解耦。
4.2.4 付款后拉租客进发布群(order/lifecycle.go)
在 ConfirmPaidFromChannelTx 现有建订单群逻辑之后(或直接替换),新增/修改为:
// 废弃原 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):
- 按
listing_id查发布群。找不到则跳过(优雅降级:历史发布无发布群,不影响下单) - 幂等插入参与者
(conversation_id, 'user', renterID, role='renter', joined_at=NOW()),OnConflict{DoNothing} - 发系统消息"租客 xxx 已加入群聊(订单 )"
- SSE 通知在事务外统一刷新,无需在此处理
关键:joined_at 字段记录租客加入时间,用于后续消息可见性过滤。
4.2.5 租客移出发布群(订单终态)
订单进入终态时,把租客移出发布群,保持群成员恒定为「号主 + 客服」,避免历史租客残留导致隐私泄露。
新函数 RemoveRenterFromListingConversation(tx, listingID, renterID, reason):
- 按
listing_id查发布群,找不到则跳过 - 删除该租客的
chat_participants行:tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ? AND role = ?", conversationID, "user", renterID, "renter").Delete(&model.ChatParticipant{}) - 发系统消息(如"订单已结束,租客已退出群聊")
触发点(在订单终态流转处调用):
| 订单事件 | 文件 | 动作 |
|---|---|---|
| 订单完成 | order/checkout_finalize.go |
移出租客 |
| 租客取消 | order/lifecycle.go 的 Cancel |
移出租客 |
| 退款终结 / 关闭 | order/refund.go / closed 流转 |
移出租客 |
注意:InTransaction 锁保证一个账号同时只有一个活跃订单,故"同时多租客共群"不会发生,移出机制主要清理历史租客。
4.2.6 消息查询增加可见性过滤(chat/message.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:
// 发布成功后
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 核心技术亮点
- 消息可见性过滤 - 通过
participant.joined_at实现租客隔离,简单可靠 - 优雅降级 - 历史数据无发布群时跳过,不阻塞下单流程
- 模块解耦 - 通过接口注入避免 listing 直接依赖 chat
- 幂等设计 - 重复调用建群/拉人接口安全无副作用
- 事务一致性 - 建群失败回滚整个发布事务,保证数据正确性
7.3 风险与缓解
| 风险 | 缓解措施 |
|---|---|
| 消息可见性过滤失效 | 后端强制校验 joined_at,前端无法绕过;增加单元测试覆盖 |
| 历史订单群兼容性 | 保留 order_id 字段,新老群并行,不做强制迁移 |
| 二维码库存耗尽 | 库存预警 + 优雅降级(建群不阻塞) |
| 客服工作量激增 | 监控客服平均会话数,必要时调整分配算法 |
八、未来扩展
8.1 可选增强(本期不做)
- 二维码自动老化 Job - 定时标记过期码为
disabled,减轻客服负担 - 订单终态统一入口 - 抽象
finalizeOrder(reason)避免移出逻辑遗漏 - 客服会话过滤 - 提前规划发布群数量激增后的工具支持
- AB 测试灰度 - 先对部分号主开启,验证企业微信引流效果
8.2 长期关注点
- SSE 推送可靠性 - 如果发布群量大,考虑引入 MQ
- 企业微信 API 对接 - 后期可研究是否有群码自动创建 API
- 消息分页性能 - 发布群长期存在,消息量可能很大,需优化查询性能
九、验收标准
9.1 功能验收
- 发布账号时自动建发布群,群内有号主、客服、欢迎语、二维码图片
- 前端发布成功后自动跳进发布群
- 租客付款后自动加入发布群,发系统消息
- 租客只能看到加入时间之后的消息(历史消息不可见)
- 订单终态后租客自动移出群,发系统消息
- 同一账号多次出租,新租客看不到之前租客的对话
- 客服可在后台上传/管理二维码,查看库存统计
- 二维码库存不足时,客服收到站内信预警
9.2 性能验收
- 发布群创建耗时 < 500ms(含二维码分配)
- 消息查询带可见性过滤,耗时无明显增加(< 100ms)
- 历史数据无发布群时,付款流程不受影响
9.3 兼容性验收
- 历史订单群正常展示和使用
- 新老群类型在前端有明确区分
- 二维码用尽时建群不阻塞,发提示文案
十、总结
单群方案通过消息可见性过滤实现了双群方案的隐私隔离效果,同时大幅降低开发和运维成本。方案契合业务约束(一个账号同时只有一个订单),避免了过度设计,是更优的技术选择。
核心价值:
- ✅ 解决「联系不到卖家」痛点(发布即建群 + 企业微信引流)
- ✅ 降低开发成本 40%,运维成本 50%
- ✅ 提升用户体验(一个群承载全生命周期)
- ✅ 保障隐私安全(消息可见性过滤)
建议下一步:开始实施提交 1(数据层 + 二维码池管理),为核心功能打好基础。