1. 案例情况:
- 鸿蒙APP接纳ArkTS语法编写,API14情况,DevEco Studio 5.0.7.210编辑器开发
- 后台接口基于SpringBoot,后台前端基于Vue开发
- 核心技能接纳 WebSocket 举行通讯
2. 主要实现功能:
- 实时聊天
- 在线实时状态检测(后台断线,APP端可实时显示状态)
3. 运行实测结果图如下:
分析:
- APP端和后台客服可以举行实时聊天
- APP端顶部[在线客服]旁边有个绿色图标,表示连接正常,如果后台关闭了,则连接不正常,这个图标会立马变成灰色,后台服务规复正常后,该图标会立马变成绿色状态
- 后台客服可以主动连接和断开连接
4. APP端代码如下:
- import webSocket from '@ohos.net.webSocket';
- import CommonConstants from '../../common/CommonConstants';
- import { tokenUtils } from '../../common/TokenUtils';
- import Logger from '../../common/utils/Logger';
- import { myTools } from '../../common/utils/MyTools';
- import { Header } from '../../component/Header';
- import { ChatModel } from '../../model/chat/ChatModel';
- //执行websocket通讯的对象
- let wsSocket = webSocket.createWebSocket()
- /**
- * 在线客服-页面
- */
- @Entry
- @Component
- struct ChatPage {
- //当前登录人的用户ID
- @State userId: number = -1;
- //要发送的信息
- @State sendMsg: string = ''
- //ws服务端地址
- @State wsServerUrl: string = "ws://" + CommonConstants.SERVER_IP + ":" + CommonConstants.SERVER_PORT + "/webSocket/"
- //与后台 WebSocket 的连接状态
- @State connectStatus: boolean = false
- scroller: Scroller = new Scroller()
- //是否绑定了事件处理程序
- eventHandleBinded: boolean = false
- @State intervalID: number = 0;
- //消息集合
- @State messageList: Array<ChatModel> = [];
- //检查连接状态
- checkStatus() {
- if (!this.connectStatus) {
- wsSocket.connect(this.wsServerUrl + this.userId)
- .then((value) => {
- })
- .catch((e: Error) => {
- this.connectStatus = false; //连接状态不可用
- });
- }
- wsSocket.send('heartbeat')
- .then((value) => {
- })
- .catch((e: Error) => {
- this.connectStatus = false; //连接状态不可用
- })
- }
- aboutToAppear(): void {
- this.userId = tokenUtils.getUserInfo().id as number;
- this.connect2Server();
- //重复执行(此处注意:setInterval里面如果需要使用this的话,就必须使用匿名函数的写法,否则取不到值)
- this.intervalID = setInterval(() => {
- this.checkStatus();
- Logger.debug('WebSocket连接状态=' + this.connectStatus)
- }, 2000);
- }
- build() {
- Row() {
- Column() {
- Stack() {
- Header({ title: '在线客服', showBack: true, backgroundColorValue: '#ffffff' })
- Image($r('app.media.svg_connectStatus'))
- .fillColor(this.connectStatus ? '#1afa29' : '#cccccc')
- .width(20)
- .offset({ x: -75 })
- }
- //展示消息区域
- Scroll(this.scroller) {
- //展示消息
- Column({ space: 30 }) {
- ForEach(this.messageList, (item: ChatModel) => {
- if (item.role == 'ai') {
- //客服展示在左侧
- Column({ space: 10 }) {
- //消息时间
- Row() {
- Text(item.createTime)
- .fontSize(11)
- .fontColor('#cccccc')
- }
- .padding({ left: 13 })
- .justifyContent(FlexAlign.Center)
- .width('100%')
- //消息和头像
- Row({ space: 5 }) {
- //头像
- Image(item.avatar)
- .width(45)
- .height(45)
- .borderRadius(3)
- //消息
- Text(item.text)
- .fontSize(14)
- .width('60%')
- .padding(12)
- .backgroundColor('#2c2c2c')
- .fontColor('#ffffff')
- .borderRadius(6)
- }
- .padding({ left: 13 })
- .justifyContent(FlexAlign.Start)
- .width('100%')
- }
- .width('100%')
- } else {
- //用户自己展示在右侧
- Column({ space: 10 }) {
- //消息时间
- Row() {
- Text(item.createTime)
- .fontSize(11)
- .fontColor('#cccccc')
- }
- .padding({ right: 13 })
- .justifyContent(FlexAlign.Center)
- .width('100%')
- //消息和头像
- Row({ space: 5 }) {
- //消息
- Text(item.text)
- .fontSize(14)
- .width('60%')
- .padding(12)
- .backgroundColor('#1afa29')
- .fontColor('#141007')
- .borderRadius(6)
- //头像
- Image(item.avatar)
- .width(45)
- .height(45)
- .borderRadius(3)
- }
- .padding({ right: 13 })
- .justifyContent(FlexAlign.End)
- .width('100%')
- }
- .width('100%')
- }
- })
- }
- .width('100%')
- .padding({ top: 20, bottom: 20 })
- }
- .align(Alignment.Top)
- .layoutWeight(1)
- .flexGrow(1)
- .scrollable(ScrollDirection.Vertical)
- .scrollBar(BarState.On)
- .scrollBarWidth(5)
- //发送消息输入框
- Flex({ justifyContent: FlexAlign.End, alignItems: ItemAlign.Center }) {
- TextInput({ text: this.sendMsg, placeholder: "请输入消息..." })
- .flexGrow(1)
- .borderRadius(1)
- .onChange((value) => {
- this.sendMsg = value
- })
- Button("发送", { type: ButtonType.Normal, stateEffect: true })
- .enabled(this.connectStatus)
- .width(90)
- .fontSize(17)
- .margin({ left: 5 })
- .flexGrow(0)
- .onClick(() => {
- if (!this.sendMsg) {
- myTools.alertMsg('发送消息不能为空!');
- return;
- }
- this.sendMsg2Server()
- })
- }
- .width('100%')
- .padding(3)
- }
- .width('100%')
- .justifyContent(FlexAlign.Start)
- .height('100%')
- }
- .height('100%')
- .padding({ top: CommonConstants.TOP_PADDING, bottom: CommonConstants.BOTTOM_PADDING })
- }
- //发送消息到服务端
- sendMsg2Server() {
- wsSocket.send(this.sendMsg)
- .then((value) => {
- })
- .catch((e: Error) => {
- this.connectStatus = false; //连接状态不可用
- })
- this.scroller.scrollEdge(Edge.Bottom);
- this.sendMsg = ''; //清空消息
- }
- //连接服务端
- connect2Server() {
- this.bindEventHandle()
- wsSocket.connect(this.wsServerUrl + this.userId)
- .then((value) => {
- })
- .catch((e: Error) => {
- this.connectStatus = false; //连接状态不可用
- });
- }
- }
复制代码 5. 后台接口核心代码如下:
- package cn.wujiangbo.WebSocket.server;
- import cn.hutool.core.util.ObjectUtil;
- import cn.wujiangbo.WebSocket.config.GetHttpSessionConfig;
- import cn.wujiangbo.WebSocket.pojo.ClientInfoEntity;
- import cn.wujiangbo.WebSocket.pojo.IM;
- import cn.wujiangbo.domain.app.AppUser;
- import cn.wujiangbo.service.app.AppUserService;
- import cn.wujiangbo.util.DateUtils;
- import cn.wujiangbo.util.SpringContextUtil;
- import com.aliyun.oss.ServiceException;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.stereotype.Component;
- import org.springframework.web.bind.annotation.CrossOrigin;
- import javax.annotation.PostConstruct;
- import javax.websocket.*;
- import javax.websocket.server.PathParam;
- import javax.websocket.server.ServerEndpoint;
- import java.io.IOException;
- import java.text.SimpleDateFormat;
- import java.time.LocalDateTime;
- import java.util.Date;
- import java.util.Iterator;
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
- /**
- * <p>该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作。</p>
- */
- @Slf4j
- @Component
- @CrossOrigin(origins = "*")
- @ServerEndpoint(value = "/webSocket/{userId}", configurator = GetHttpSessionConfig.class)
- public class WebSocketServer {
- /**
- * key:客户端连接唯一标识(用户ID)
- * value:ClientInfoEntity
- */
- private static final Map<Long, ClientInfoEntity> uavWebSocketInfoMap = new ConcurrentHashMap<Long, ClientInfoEntity>();
- //默认连接2小时
- private static final int EXIST_TIME_HOUR = 2;
- AppUserService appUserService;
- //客服头像地址(替换成网络可访问的图片地址即可)
- private String CUSTOMER_IAMGE = "";
- /**
- * 连接建立成功调用的方法
- *
- * @param session 第一个参数必须是session
- * @param sec
- * @param userId 代表客户端的唯一标识
- */
- @OnOpen
- public void onOpen(Session session, EndpointConfig sec, @PathParam("userId") Long userId) {
- if (uavWebSocketInfoMap.containsKey(userId)) {
- throw new ServiceException("token已建立连接");
- }
- //把成功建立连接的会话在实体类中保存
- ClientInfoEntity entity = new ClientInfoEntity();
- entity.setUserId(userId);
- entity.setSession(session);
- //默认连接N个小时
- entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
- uavWebSocketInfoMap.put(userId, entity);
- //之所以获取http session 是为了获取获取 httpsession 中的数据 (用户名/账号/信息)
- System.out.println("WebSocket 连接建立成功,userId=: " + userId);
- }
- /**
- * 当断开连接时调用该方法
- */
- @OnClose
- public void onClose(Session session, @PathParam("userId") Long userId) {
- // 找到关闭会话对应的用户 ID 并从 uavWebSocketInfoMap 中移除
- if (ObjectUtil.isNotEmpty(userId) && uavWebSocketInfoMap.containsKey(userId)) {
- uavWebSocketInfoMap.remove(userId);
- System.out.println("WebSocket 连接关闭成功,userId=: " + userId);
- }
- }
- /**
- * 接受消息
- * 这是接收和处理来自用户的消息的地方。我们需要在这里处理消息逻辑,可能包括广播消息给所有连接的用户。
- */
- @OnMessage
- public void onMessage(Session session, @PathParam("userId") Long userId, String message) throws IOException {
- log.info("接收到来自 [" + userId + "] 的消息:" + message);
- //如果是心跳检测的话,直接返回success即可表示,后台服务是正常状态
- if ("heartbeat".equals(message)) {
- this.sendUserMessage(userId, "success");
- return;
- }
- ClientInfoEntity entity = uavWebSocketInfoMap.get(userId);
- if (entity == null) {
- this.sendUserMessage(userId, "用户在线信息错误!");
- return;
- }
- IM im = new IM();
- if (userId != -1) {
- appUserService = SpringContextUtil.getBean(AppUserService.class);
- AppUser user = appUserService.getById(userId);
- if (user == null) {
- this.sendUserMessage(userId, "用户信息不存在!");
- return;
- }
- im.setRole("user");//user表示APP用户发的消息
- im.setUsername(user.getNickName());
- im.setAvatar(user.getUserImg());
- } else {
- im.setRole("ai");//ai表示后台客服发的消息
- im.setUsername("人工客服");
- im.setAvatar(CUSTOMER_IAMGE);
- }
- im.setUid(userId);
- im.setCreateTime(DateUtils.getCurrentDateString());
- im.setText(message);
- //只要接受到客户端的消息就进行续命(时间)
- entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
- uavWebSocketInfoMap.put(userId, entity);
- String jsonStr = new ObjectMapper().writeValueAsString(im); // 处理后的消息体
- this.sendMessage(jsonStr);
- }
- /**
- * 处理WebSocket中发生的任何异常。可以记录这些错误或尝试恢复。
- */
- @OnError
- public void onError(Throwable error) {
- log.error("报错信息:" + error.getMessage());
- error.printStackTrace();
- }
- private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy:MM:dd hh:mm:ss");
- /**
- * 发送消息定时器
- * 开启定时任务,每隔N秒向前台发送一次时间
- */
- @PostConstruct
- // @Scheduled(cron = "0/59 * * * * ? ")
- public void refreshDate() {
- //当没有客户端连接时阻塞等待
- if (!uavWebSocketInfoMap.isEmpty()) {
- //超过存活时间进行删除
- Iterator<Map.Entry<Long, ClientInfoEntity>> iterator = uavWebSocketInfoMap.entrySet().iterator();
- while (iterator.hasNext()) {
- Map.Entry<Long, ClientInfoEntity> entry = iterator.next();
- if (entry.getValue().getExistTime().compareTo(LocalDateTime.now()) <= 0) {
- log.info("WebSocket " + entry.getKey() + " 已到存活时间,自动断开连接");
- try {
- entry.getValue().getSession().close();
- } catch (IOException e) {
- log.error("WebSocket 连接关闭失败: " + entry.getKey() + " - " + e.getMessage());
- }
- //过期则进行移除
- iterator.remove();
- }
- }
- sendMessage(FORMAT.format(new Date()));
- }
- }
- /**
- * 群发信息的方法
- *
- * @param message 消息
- */
- public void sendMessage(String message) {
- System.out.println("给所有APP用户发送消息:" + message + ",时间:" + DateUtils.getCurrentDateString());
- //循环客户端map发送消息
- uavWebSocketInfoMap.values().forEach(item -> {
- //向每个用户发送文本信息。这里getAsyncRemote()解释一下,向用户发送文本信息有两种方式,
- // 一种是getBasicRemote,一种是getAsyncRemote
- //区别:getAsyncRemote是异步的,不会阻塞,而getBasicRemote是同步的,会阻塞,由于同步特性,第二行的消息必须等待第一行的发送完成才能进行。
- // 而第一行的剩余部分消息要等第二行发送完才能继续发送,所以在第二行会抛出IllegalStateException异常。所以如果要使用getBasicRemote()同步发送消息
- // 则避免尽量一次发送全部消息,使用部分消息来发送
- item.getSession().getAsyncRemote().sendText(message);
- });
- }
- /**
- * 给指定用户发送消息
- */
- public void sendUserMessage(Long userId, String message) throws IOException {
- System.out.println("给APP用户 [" + userId + "] 发送消息:" + message + ",时间:" + DateUtils.getCurrentDateString());
- ClientInfoEntity clientInfoEntity = uavWebSocketInfoMap.get(userId);
- if (clientInfoEntity != null && clientInfoEntity.getSession() != null) {
- if (clientInfoEntity.getSession().isOpen()) {
- clientInfoEntity.getSession().getBasicRemote().sendText(message);
- }
- }
- }
- }
复制代码
6. 规划
目前实现的功能非常有限,仅仅是一个基础的Demo,后面会基于这个出版,做一些迭代开发,规划如下:
- 后台客服聊天页面,做一个APP端用户列表,可以选择和指定的用户聊天
- APP端做一个好友列表,然后好友之间可以互相聊天
- 支持发送基本的表情
有爱好的可以加入!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |