# 审计之家 · 在线客服系统 · 前端对接与接入使用说明 > 本文档面向前端开发人员,说明如何将三端(微信小程序、PC 平台坐席端、PC 商家端)接入在线客服系统后端,实现实时消息通信。 --- ## 🌐 一、系统整体结构说明 ``` ┌─────────────────┐ WebSocket + REST API ┌──────────────────────┐ │ 微信小程序端 │ ◄──────────────────────► │ │ │ pages/chat/ │ │ Java 后端服务 │ │ chat.vue │ │ Spring Boot 2.7 │ └─────────────────┘ │ 端口: 8080 │ │ │ ┌─────────────────┐ WebSocket + REST API │ ws://host:8080/ │ │ PC 平台坐席端 │ ◄──────────────────────► │ api/chat/ws/chat │ │ ChatWorkbench │ │ │ │ .vue │ └──────────────────────┘ └─────────────────┘ │ ┌─────────────────┐ WebSocket + REST API │ │ PC 商家端 │ ◄──────────────────────────────────┘ │ merchant Chat │ │ .vue │ └─────────────────┘ ``` **三端的差异说明:** | 特性 | 小程序用户端 | PC 坐席端 | PC 商家端 | |---|---|---|---| | 会话类型(sessionType) | `1` | — | `2` | | 可发送岗位卡片 | ❌ 仅接收 | ✅ 可发送 | ❌ 仅接收 | | 可发送结算单 | ❌ 仅查看/支付 | ✅ 可发送 | ❌ | | 可发图片/附件 | ✅ | ✅ | ✅ | | 右侧显示订单 | 显示测评/培训订单 | 显示对应用户/商家订单 | — | --- ## 🔑 二、接口基础信息 ``` 后端服务地址(开发环境): REST API: http://localhost:8080/api/chat WebSocket: ws://localhost:8080/api/chat/ws/chat API 文档: http://localhost:8080/api/chat/doc.html 请求头(认证): Authorization: Bearer {JWT_TOKEN} 数据格式: application/json ``` --- ## 📦 三、前端依赖安装 ### PC 端(Vue 3 项目) 在 `pc-web-preview` 和 `merchant-chat-pc` 项目中安装 STOMP 客户端库: ```bash npm install @stomp/stompjs sockjs-client ``` ### 小程序端(UniApp) UniApp 中通过 `uni.connectSocket` 原生 API 建立 WebSocket,无需额外安装库。 --- ## 🔗 四、WebSocket 连接实现 ### 4.1 PC 端(Vue 3 + STOMP)封装示例 建议在项目中创建 `src/utils/chatSocket.js`: ```javascript import { Client } from '@stomp/stompjs'; import SockJS from 'sockjs-client'; let stompClient = null; let onMessageCallback = null; // 收到消息的回调 /** * 建立 WebSocket 连接 * @param {string} token JWT 登录令牌 * @param {string} sessionNo 会话编号(从创建会话接口获取) * @param {Function} onMessage 收到消息时的回调函数 */ export function connectChat(token, sessionNo, onMessage) { onMessageCallback = onMessage; stompClient = new Client({ // 使用 SockJS 兼容层连接 webSocketFactory: () => new SockJS('http://localhost:8080/api/chat/ws/chat'), // 携带认证 Token connectHeaders: { Authorization: 'Bearer ' + token }, // 重连间隔(毫秒) reconnectDelay: 5000, // 连接建立后订阅频道 onConnect: () => { console.log('[ChatSocket] 连接成功'); // 订阅本会话的消息频道(双端共用同一频道) stompClient.subscribe(`/topic/session/${sessionNo}`, (frame) => { const data = JSON.parse(frame.body); if (onMessageCallback) { onMessageCallback(data); } }); // 订阅个人通知频道(新消息提醒、已读回执等) stompClient.subscribe('/user/queue/notify', (frame) => { const data = JSON.parse(frame.body); handleNotify(data); }); }, onDisconnect: () => { console.log('[ChatSocket] 连接已断开'); }, onStompError: (frame) => { console.error('[ChatSocket] 错误:', frame.headers['message']); } }); stompClient.activate(); } /** * 通过 WebSocket 发送文本消息 * @param {number} sessionId 会话ID * @param {string} content 消息内容 */ export function sendTextByWs(sessionId, content) { if (!stompClient || !stompClient.connected) { console.warn('[ChatSocket] 未连接,无法发送消息'); return; } stompClient.publish({ destination: '/app/chat/send', body: JSON.stringify({ msgType: 'text', sessionId: sessionId, msgNo: generateMsgNo(), // 客户端生成唯一消息编号(防重发) content: content }) }); } /** * 断开连接(页面销毁时调用) */ export function disconnectChat() { if (stompClient) { stompClient.deactivate(); stompClient = null; } } /** 处理系统通知(结算单失效、已读回执等) */ function handleNotify(data) { console.log('[ChatSocket] 收到通知:', data); // 根据 type 分发处理 // ORDER_CARD_EXPIRE: 结算单失效 // READ_RECEIPT: 消息已读 } /** 生成唯一消息编号 */ function generateMsgNo() { return 'MSG_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8); } ``` ### 4.2 在 Vue 组件中使用 ```vue ``` ### 4.3 微信小程序端(UniApp)WebSocket 连接 ```javascript // pages/chat/chat.vue let socketTask = null; /** 建立 WebSocket 连接(小程序原生 API) */ function connectSocket(sessionNo, token) { socketTask = uni.connectSocket({ // 注意:小程序不支持 SockJS,直接使用原生 WebSocket url: `ws://你的服务器IP:8080/api/chat/ws/chat/${sessionNo}?token=${token}`, header: { Authorization: 'Bearer ' + token } }); socketTask.onOpen(() => { console.log('WebSocket 连接成功'); // 发送订阅信令(STOMP 格式) socketTask.send({ data: `SUBSCRIBE\ndestination:/topic/session/${sessionNo}\n\n\0` }); }); socketTask.onMessage((res) => { const data = JSON.parse(res.data); handleIncomingMessage(data); }); socketTask.onClose(() => { console.log('WebSocket 已关闭,尝试重连...'); setTimeout(() => connectSocket(sessionNo, token), 3000); }); } /** 发送文本消息 */ function sendMessage(sessionId, content) { const payload = JSON.stringify({ msgType: 'text', sessionId: sessionId, msgNo: 'MSG_' + Date.now(), content: content }); socketTask.send({ data: payload }); } /** 页面卸载时关闭连接 */ onUnmounted(() => { if (socketTask) socketTask.close(); }); ``` --- ## 📡 五、REST API 对接 ### 5.1 创建/进入会话(进入聊天页第一步) **小程序用户进入聊天页时调用:** ```javascript // sessionType: 1 = 小程序用户 async function createOrGetSession(userId, userName, avatar) { const res = await fetch('http://localhost:8080/api/chat/session/create', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ sessionType: 1, // 小程序用户 fromUserId: userId, fromUserName: userName, fromUserAvatar: avatar }) }); const data = await res.json(); // data.data.sessionNo → 用于建立 WebSocket 订阅 // data.data.sessionId → 用于后续消息发送 return data.data; } ``` **商家端进入聊天时调用(sessionType 传 2):** ```javascript body: JSON.stringify({ sessionType: 2, // PC 商家 fromUserId: merchantId, fromUserName: merchantName, fromUserAvatar: merchantLogo }) ``` --- ### 5.2 加载历史消息 ```javascript async function loadHistory(sessionId, beforeMsgId = null) { const params = new URLSearchParams({ sessionId, page: 1, pageSize: 50, ...(beforeMsgId ? { beforeMsgId } : {}) }); const res = await fetch(`http://localhost:8080/api/chat/message/history?${params}`, { headers: { 'Authorization': 'Bearer ' + token } }); const data = await res.json(); // data.data.rows → 消息列表(按时间倒序) return data.data.rows.reverse(); // 翻转为正序显示 } ``` --- ### 5.3 上传并发送图片 ```javascript // PC 端 async function sendImage(sessionId, file) { const formData = new FormData(); formData.append('sessionId', sessionId); formData.append('msgNo', 'MSG_' + Date.now()); formData.append('file', file); const res = await fetch('http://localhost:8080/api/chat/message/send/image', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData }); return res.json(); } // 小程序端(UniApp) function sendImageMini(sessionId) { uni.chooseImage({ count: 1, success: (res) => { const tempFilePath = res.tempFilePaths[0]; uni.uploadFile({ url: 'http://你的服务器IP:8080/api/chat/message/send/image', filePath: tempFilePath, name: 'file', formData: { sessionId: sessionId, msgNo: 'MSG_' + Date.now() }, header: { Authorization: 'Bearer ' + token }, success: (uploadRes) => { console.log('图片发送成功', JSON.parse(uploadRes.data)); } }); } }); } ``` --- ### 5.4 坐席端发送岗位卡片 > **仅 PC 坐席端可调用此接口,商家端无此功能。** ```javascript async function sendJobCard(sessionId, jobInfo) { const res = await fetch('http://localhost:8080/api/chat/message/send/job-card', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ sessionId: sessionId, msgNo: 'MSG_' + Date.now(), senderId: currentWaiterId, // 当前坐席客服 ID payload: { jobId: jobInfo.id, title: jobInfo.title, salary: jobInfo.salary, tags: jobInfo.tags, company: jobInfo.company, location: jobInfo.location, urgency: jobInfo.urgency, quota: jobInfo.quota, deadline: jobInfo.deadline } }) }); return res.json(); } ``` --- ### 5.5 坐席端发送结算单(60 秒倒计时) > **仅 PC 坐席端向小程序用户发送**,商家沟通中无结算单功能。 ```javascript async function sendOrderCard(sessionId, orderInfo) { const res = await fetch('http://localhost:8080/api/chat/message/send/order-card', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ sessionId: sessionId, msgNo: 'MSG_' + Date.now(), senderId: currentWaiterId, payload: { orderName: orderInfo.name, // 结算单项目名称 orderPrice: orderInfo.price, // 金额(数字类型) orderType: orderInfo.type // 类型描述(可选) } }) }); const data = await res.json(); // 响应中包含 expireTime(过期时间)和 countdownSeconds(60) // 前端开始本地倒计时显示 return data.data; } ``` --- ### 5.6 小程序用户支付结算单 ```javascript async function payOrderCard(orderCardId, userId) { const res = await fetch( `http://localhost:8080/api/chat/message/order-card/${orderCardId}/pay?userId=${userId}`, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } } ); const data = await res.json(); if (data.code === 200) { // 支付成功,更新本地 UI 状态为"已支付" } else { // 支付失败(可能已失效) uni.showToast({ title: data.msg, icon: 'error' }); } } ``` --- ## ⏱️ 六、结算单 60 秒倒计时实现 ### 前端倒计时逻辑(适用于三端) ```javascript /** * 启动结算单本地倒计时(接收到 order_card 消息后调用) * @param {object} msg 消息对象 */ function startOrderCardCountdown(msg) { if (msg.msgType !== 'order_card') return; if (msg.payload.status !== 'pending') return; const expireTime = new Date(msg.payload.expireTime).getTime(); const timer = setInterval(() => { const remaining = Math.floor((expireTime - Date.now()) / 1000); if (remaining <= 0) { clearInterval(timer); msg.payload.countdown = 0; // 若服务端尚未推送失效通知,本地先更新 UI if (msg.payload.status === 'pending') { msg.payload.status = 'expired'; } } else { msg.payload.countdown = remaining; } }, 1000); // 组件销毁时清除定时器(防内存泄漏) onUnmounted(() => clearInterval(timer)); } ``` ### 注意事项 - 前端倒计时仅为 **UI 展示辅助**,实际过期判断以**服务端定时任务**为准 - 服务端每 5 秒扫描一次,过期后通过 WebSocket 推送 `ORDER_CARD_EXPIRE` 事件 - 前端应优先监听 WebSocket 事件,而非依赖本地倒计时结果 --- ## 📨 七、消息渲染规范 ### 消息类型(msgType)完整列表 | msgType | 说明 | 渲染组件 | |---|---|---| | `text` | 文本消息 | 气泡文字 | | `emoji` | 表情消息 | 表情图标气泡 | | `image` | 图片消息 | 图片缩略图,点击放大 | | `file` | 附件消息 | 文件卡片(图标+名称+大小),支持预览/下载 | | `job_card` | 岗位推荐卡片 | 卡片组件(仅坐席发送,小程序接收) | | `order_card` | 结算单卡片 | 含倒计时/状态标签+支付按钮 | ### 发送方判断(senderType) | senderType | 含义 | 气泡方向 | |---|---|---| | `1` | 用户(小程序)或商家 | 右侧显示 | | `2` | 坐席客服 | 左侧显示 | | `3` | 系统消息 | 居中灰色小字 | ### 结算单状态显示 | status(payload内) | 中文显示 | 颜色 | 按钮 | |---|---|---|---| | `pending` | 倒计时(60s → 0s) | 橙色 | 显示"立即支付" | | `expired` | 已失效 | 灰色 | 显示"已失效"(不可点击) | | `paid` | 已支付 | 绿色 | 显示"已支付" | | `cancelled` | 已取消 | 灰色 | — | --- ## 🎫 八、坐席配置管理接口对接 > **仅 PC 坐席端后台管理使用**(`SeatConfig.vue`) ```javascript const BASE = 'http://localhost:8080/api/chat/seat'; const headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }; // 获取坐席列表 const list = await fetch(`${BASE}/list?page=1&pageSize=10`, { headers }).then(r => r.json()); // 新增坐席 await fetch(BASE, { method: 'POST', headers, body: JSON.stringify({ seatName: '客服小D', module: '小程序', status: 1, waiterIds: [1001] }) }); // 上传坐席头像(先上传获取URL,再提交表单) const formData = new FormData(); formData.append('file', avatarFile); const avatarRes = await fetch(`${BASE}/avatar/upload`, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, // 注意:不加 Content-Type,让浏览器自动设置边界 body: formData }).then(r => r.json()); // avatarRes.data.avatarUrl → 头像 URL,存入表单后一起提交 ``` --- ## 🔒 九、商家端与用户端的差异隔离 根据业务规则,商家端(PC)与小程序用户端在功能上有明确隔离: **商家端(sessionType = 2):** - ✅ 可发送:文本、图片、附件 - ❌ 不可发送:岗位卡片、结算单卡片 - ✅ 界面右侧显示:商家购买的套餐订单 **小程序用户端(sessionType = 1):** - ✅ 系统自动发送欢迎消息(岗位卡片 + 文字 + 结算单示例) - ✅ 可接收并支付结算单 - ✅ 界面右侧显示:用户的测评/培训订单 **坐席端(PC):** - 切换到商家会话时,**隐藏"发送卡片"按钮** - 切换到用户会话时,**显示"发送卡片"按钮**(包含岗位推荐、结算单) ### 前端判断示例(ChatWorkbench.vue) ```javascript // 根据当前会话类型控制发送卡片按钮的显示 const isMerchantSession = computed(() => activeSession.value?.type === 'merchant'); // 模板中: // 发送卡片 ``` --- ## 🌐 十、生产环境部署注意事项 | 配置项 | 开发环境 | 生产环境 | |---|---|---| | 后端地址 | `http://localhost:8080/api/chat` | `https://你的域名/api/chat` | | WebSocket 地址 | `ws://localhost:8080/api/chat/ws/chat` | `wss://你的域名/api/chat/ws/chat` | | 小程序合法域名 | — | 需在微信公众平台配置 `request`、`socket` 合法域名 | | HTTPS | 不需要 | 必须(微信小程序要求) | | JWT 密钥 | 开发用密钥 | 修改为强密钥(≥32位随机字符串) | ### 前端修改接口地址 在各前端项目中创建或修改环境变量文件: ```bash # pc-web-preview/.env.production VITE_API_BASE = https://你的域名/api/chat VITE_WS_URL = wss://你的域名/api/chat/ws/chat ``` ```javascript // 在 chatSocket.js 中: webSocketFactory: () => new SockJS(import.meta.env.VITE_WS_URL || 'http://localhost:8080/api/chat/ws/chat') ``` --- ## 📋 十一、接入自检清单 完成接入后,请按以下流程验证功能: **基础消息流程:** - [ ] 小程序用户进入聊天页,系统自动推送欢迎消息(岗位卡片 + 文字 + 结算单) - [ ] 坐席端左侧会话列表出现该用户的会话 - [ ] 双端均可发送和接收文本消息(实时推送) - [ ] 双端均可发送图片,对方实时收到并可点击查看大图 - [ ] 双端均可发送附件,对方可预览和下载 **结算单流程:** - [ ] 坐席端发送结算单后,小程序端立即收到并显示 60 秒倒计时 - [ ] 60 秒内未支付,双端卡片自动变为"已失效"状态 - [ ] 60 秒内点击"立即支付",双端同步变为"已支付"状态 **商家沟通流程:** - [ ] 商家端进入聊天,对话内容不出现岗位卡片或结算单 - [ ] 坐席端切换至商家会话,"发送卡片"按钮不显示 - [ ] 双端可正常发送文本、图片、附件 **坐席管理流程:** - [ ] 可新增/编辑/删除坐席 - [ ] 上传坐席头像后,列表中正确显示头像 - [ ] 切换坐席启用状态有效 --- *文档版本:v1.0 · 2026-03-31*