# 审计之家 · 在线客服系统 · 前端对接与接入使用说明
> 本文档面向前端开发人员,说明如何将三端(微信小程序、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*