在线沟通对接接入说明.md 20 KB

审计之家 · 在线客服系统 · 前端对接与接入使用说明

本文档面向前端开发人员,说明如何将三端(微信小程序、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-previewmerchant-chat-pc 项目中安装 STOMP 客户端库:

npm install @stomp/stompjs sockjs-client

小程序端(UniApp)

UniApp 中通过 uni.connectSocket 原生 API 建立 WebSocket,无需额外安装库。


🔗 四、WebSocket 连接实现

4.1 PC 端(Vue 3 + STOMP)封装示例

建议在项目中创建 src/utils/chatSocket.js

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 组件中使用

<script setup>
import { onMounted, onUnmounted } from 'vue';
import { connectChat, sendTextByWs, disconnectChat } from '@/utils/chatSocket';

const props = defineProps({ sessionNo: String, sessionId: Number });
const messages = ref([]);

onMounted(() => {
  const token = localStorage.getItem('token');  // 获取登录 token

  // 建立 WebSocket 连接
  connectChat(token, props.sessionNo, (msg) => {
    handleIncomingMessage(msg);
  });

  // 加载历史消息
  loadHistory();
});

onUnmounted(() => {
  disconnectChat();  // 页面销毁时断开连接
});

/** 处理收到的消息(统一分发) */
function handleIncomingMessage(data) {
  switch (data.type) {
    case 'MSG':
      // 普通消息,添加到消息列表
      messages.value.push(data);
      break;
    case 'ORDER_CARD_EXPIRE':
      // 结算单失效,找到对应消息并更新状态
      updateOrderCardStatus(data.orderCardId, 'expired');
      break;
    case 'ORDER_CARD_PAID':
      // 结算单已支付
      updateOrderCardStatus(data.orderCardId, 'paid');
      break;
    case 'READ_RECEIPT':
      // 对方已读
      console.log('消息已读到:', data.lastReadMsgId);
      break;
  }
}

/** 更新结算单卡片状态 */
function updateOrderCardStatus(orderCardId, status) {
  const msg = messages.value.find(m =>
    m.msgType === 'order_card' && m.payload?.orderCardId === orderCardId
  );
  if (msg) {
    msg.payload.status = status;
    msg.payload.countdown = 0;
  }
}
</script>

4.3 微信小程序端(UniApp)WebSocket 连接

// 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 创建/进入会话(进入聊天页第一步)

小程序用户进入聊天页时调用:

// 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):

body: JSON.stringify({
  sessionType: 2,            // PC 商家
  fromUserId: merchantId,
  fromUserName: merchantName,
  fromUserAvatar: merchantLogo
})

5.2 加载历史消息

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 上传并发送图片

// 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 坐席端可调用此接口,商家端无此功能。

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 坐席端向小程序用户发送,商家沟通中无结算单功能。

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 小程序用户支付结算单

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 秒倒计时实现

前端倒计时逻辑(适用于三端)

/**
 * 启动结算单本地倒计时(接收到 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

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)

// 根据当前会话类型控制发送卡片按钮的显示
const isMerchantSession = computed(() => activeSession.value?.type === 'merchant');

// 模板中:
// <el-button v-if="!isMerchantSession" @click="sendCard">发送卡片</el-button>

🌐 十、生产环境部署注意事项

配置项 开发环境 生产环境
后端地址 http://localhost:8080/api/chat https://你的域名/api/chat
WebSocket 地址 ws://localhost:8080/api/chat/ws/chat wss://你的域名/api/chat/ws/chat
小程序合法域名 需在微信公众平台配置 requestsocket 合法域名
HTTPS 不需要 必须(微信小程序要求)
JWT 密钥 开发用密钥 修改为强密钥(≥32位随机字符串)

前端修改接口地址

在各前端项目中创建或修改环境变量文件:

# pc-web-preview/.env.production
VITE_API_BASE = https://你的域名/api/chat
VITE_WS_URL   = wss://你的域名/api/chat/ws/chat
// 在 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