Merge pull request '优化客服模块' (#112) from sjy-two into master
All checks were successful
continuous-integration/drone Build is passing

Reviewed-on: #112
This commit is contained in:
root 2024-11-08 12:10:36 +08:00
commit c1a5340a73
15 changed files with 246 additions and 35 deletions

View File

@ -447,6 +447,8 @@
}
await sendMessage(msg)
messages.success('转接成功')
conversation.value = null
} finally {
// todo
}

View File

@ -153,30 +153,93 @@
// ======================= WebSocket start =======================
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' +
getRefreshToken() // 使 getRefreshToken() 使 getAccessToken() WebSocket 便访
) // WebSocket
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' +
getRefreshToken() // 使 getRefreshToken() 使 getAccessToken() WebSocket 便访
) // WebSocket
const chick = ref('2')
const userInfo = async () =>{
chick.value = '1'
user.value = await UserApi.getUserInfo(userId.value)
}
const zuoji = () =>{
chick.value = '2'
// keFuChatBoxRef.value?.getNewMessageList(conversations.value)
memberBrowsingHistoryRef.value?.initHistory(conversations.value)
}
let a = 0;
/** 发起 WebSocket 连接 */
const {data, close, open} = useWebSocket(server.value, {
autoReconnect: false,
heartbeat: true
})
const {status, data, send, open, close} = useWebSocket(server.value, {
onConnected: function (ws) {
console.log('websocket 连接成功!', ws);
},
onDisconnected: function (ws, event) {
console.log('WebSocket 连接断开', event);
},
onError: function (ws, event) {
console.error('WebSocket 连接错误:', event);
if (event instanceof ErrorEvent) {
console.error('详细错误信息:', event.message);
} else {
console.error('非标准错误:', event);
}
},
onMessage: function (ws, event) {
console.log('收到的 WebSocket 消息:', event.data);
a = a + 1 ;
if(a == 2){
getConversationList()
keFuChatBoxRef.value?.refreshMessageList()
a = 0;
}
//
// if (event.data) {
// let parsedData;
// //
// try {
// parsedData = JSON.parse(event.data);
// } catch (error) {
// console.error('JSON :', error, ':', event.data);
// return; //
// }
// // type
// const { type } = parsedData;
// if (!type) {
// console.warn(':', parsedData);
// return;
// }
// //
// switch (type) {
// case 'pong':
// //
// console.log(' ping ');
// // pong
// send(JSON.stringify({ type: 'pong' }));
// break;
// case 'chat':
// //
// console.log(':', parsedData.content);
// //
// break;
// case 'notification':
// //
// console.log(':', parsedData.message);
// //
// break;
// default:
// console.warn(':', type);
// break;
// }
// } else {
// console.warn(':', event);
// }
},
autoReconnect: false, //
heartbeat: true
});
/** 监听 WebSocket 数据 */
watchEffect(() => {
@ -188,7 +251,7 @@
if (data.value === 'pong') {
return
}
// 2.1 type
const jsonMessage = JSON.parse(data.value)
const type = jsonMessage.type
@ -215,6 +278,8 @@
console.error(error)
}
})
// ======================= WebSocket end =======================
/** 加载会话列表 */
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
@ -237,18 +302,32 @@
window.close();
// window.location.href = '/kefu/support-staff';
}
const chick = ref('2')
const userInfo = async () =>{
chick.value = '1'
user.value = await UserApi.getUserInfo(userId.value)
}
const zuoji = () =>{
chick.value = '2'
// keFuChatBoxRef.value?.getNewMessageList(conversations.value)
memberBrowsingHistoryRef.value?.initHistory(conversations.value)
}
/** 初始化 */
onMounted(() => {
getConversationList()
// websocket
open()
console.log('WebSocket 已初始化');
})
/** 销毁 */
onBeforeUnmount(() => {
// websocket
close()
console.log('WebSocket 已关闭');
})
</script>

View File

@ -29,6 +29,6 @@ public class WebSocketProperties {
* 可选值localredisrocketmqkafkarabbitmq
*/
@NotNull(message = "WebSocket 的消息发送者不能为空")
private String senderType = "local";
private String senderType = "redis";
}

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
import cn.iocoder.yudao.framework.websocket.core.security.WebSocketAuthorizeRequestsCustomizer;
import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
@ -76,10 +77,15 @@ public class YudaoWebSocketAutoConfiguration {
return new WebSocketSessionManagerImpl();
}
@Bean
public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
}
// ==================== Sender 相关 ====================
@Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local")
public class LocalWebSocketMessageSenderConfiguration {
@Bean
@ -90,7 +96,7 @@ public class YudaoWebSocketAutoConfiguration {
}
@Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis")
public class RedisWebSocketMessageSenderConfiguration {
@Bean
@ -108,7 +114,7 @@ public class YudaoWebSocketAutoConfiguration {
}
@Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true)
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq")
public class RocketMQWebSocketMessageSenderConfiguration {
@Bean
@ -127,7 +133,7 @@ public class YudaoWebSocketAutoConfiguration {
}
@Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true)
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq")
public class RabbitMQWebSocketMessageSenderConfiguration {
@Bean
@ -156,7 +162,7 @@ public class YudaoWebSocketAutoConfiguration {
}
@Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true)
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka")
public class KafkaWebSocketMessageSenderConfiguration {
@Bean

View File

@ -21,4 +21,4 @@ public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCusto
registry.antMatchers(webSocketProperties.getPath()).permitAll();
}
}
}

View File

@ -43,7 +43,7 @@ public interface WebSocketSenderApi {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObject(Integer userType, String messageType, Object messageContent) {
default void sendObject(Integer userType, String messageType, Object messageContent) { //用户发送消息
send(userType, messageType, JsonUtils.toJsonString(messageContent));
}

View File

@ -79,6 +79,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-system-biz</artifactId>
<version>2.1.0-jdk8-snapshot</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -28,4 +28,8 @@ public class AppDiyTemplatePropertyRespVO {
@JsonRawValue
private String user;
private String goodsType;
private String themeType;
}

View File

@ -61,4 +61,11 @@ public class DiyTemplateDO extends BaseDO {
*/
private String property;
@TableField(exist = false)
private String goodsType;
@TableField(exist = false)
private String themeType;
}

View File

@ -11,12 +11,15 @@ import cn.iocoder.yudao.module.promotion.convert.diy.DiyPageConvert;
import cn.iocoder.yudao.module.promotion.convert.diy.DiyTemplateConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.diy.DiyTemplateDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.diy.DiyTemplateMapper;
import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
@ -36,6 +39,9 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
@Resource
private DiyPageService diyPageService;
@Resource
private DictDataApi dictDataApi;
@Transactional(rollbackFor = Exception.class)
@Override
public Long createDiyTemplate(DiyTemplateCreateReqVO createReqVO) {
@ -165,7 +171,17 @@ public class DiyTemplateServiceImpl implements DiyTemplateService {
@Override
public DiyTemplateDO getUsedDiyTemplate() {
return diyTemplateMapper.selectByUsed(true);
DiyTemplateDO diyTemplateDO = diyTemplateMapper.selectByUsed(true);
List<DictDataRespDTO> dictDataList = dictDataApi.getDictDataList("diy-template-theme");
DictDataRespDTO dictDataRespDTO = dictDataList.get(0);
diyTemplateDO.setThemeType(dictDataRespDTO.getValue());
List<DictDataRespDTO> dictDataList1 = dictDataApi.getDictDataList("diy-template-goods");
DictDataRespDTO dictDataRespDTO1 = dictDataList1.get(0);
diyTemplateDO.setGoodsType(dictDataRespDTO1.getValue());
return diyTemplateDO;
}
}

View File

@ -31,6 +31,8 @@ public interface KeFuMessageService {
* @return 编号
*/
Long sendKefuMessage(AppKeFuMessageSendReqVO sendReqVO);
String sendKefuMessageTest(String s);
/**

View File

@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessagePageReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessagePageReqVO;
@ -16,7 +17,10 @@ import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuM
import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuMessageMapper;
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyMessageDO;
import cn.iocoder.yudao.module.system.dal.mysql.notify.NotifyMessageMapper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -51,9 +55,12 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
@Resource
private WebSocketSenderApi webSocketSenderApi;
@Resource
private NotifyMessageMapper notifyMessageMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long sendKefuMessage(KeFuMessageSendReqVO sendReqVO) {
public Long sendKefuMessage(KeFuMessageSendReqVO sendReqVO) { //客服发消息
// 1.1 校验会话是否存在
KeFuConversationDO conversation = conversationService.validateKefuConversationExists(sendReqVO.getConversationId());
// 1.2 校验接收人是否存在
@ -74,20 +81,39 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
}
@Override
public Long sendKefuMessage(AppKeFuMessageSendReqVO sendReqVO) {
public Long sendKefuMessage(AppKeFuMessageSendReqVO sendReqVO) { //用户发消息
// 1.1 设置会话编号
KeFuMessageDO kefuMessage = BeanUtils.toBean(sendReqVO, KeFuMessageDO.class);
KeFuConversationDO conversation = conversationService.getOrCreateConversation(sendReqVO.getSenderId());
kefuMessage.setConversationId(conversation.getId());
// 1.2 保存消息
kefuMessage.setReceiverId(conversation.getKefuId()).setReceiverType(UserTypeEnum.ADMIN.getValue()); // 设置接收人
keFuMessageMapper.insert(kefuMessage);
// 2. 更新会话消息冗余
conversationService.updateConversationLastMessage(kefuMessage);
getSelf().sendAsyncMessageToMembers(conversation.getKefuId(), KEFU_MESSAGE_TYPE, kefuMessage);
// 3. 通知所有管理员更新对话
getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_TYPE, kefuMessage);
getSelf().sendAsyncMessageToAdmins(KEFU_MESSAGE_TYPE, kefuMessage);
return kefuMessage.getId();
}
//添加站内信
// MemberUserRespDTO user = memberUserApi.getUser(sendReqVO.getSenderId());
// NotifyMessageDO notifyMessageDO = new NotifyMessageDO()
// .setUserId(sendReqVO.getSenderId())
// .setUserType(sendReqVO.getSenderType())
// .setTemplateId((long)1)
// .setTemplateCode()
// .setTemplateNickname(user.getNickname())
// .setTemplateContent("客户发来消息")
// .setTemplateType(2)
// .setTemplateParams()
// .setReadStatus(2);
// notifyMessageMapper.insert(notifyMessageDO);
@Override
public String sendKefuMessageTest(String s){
webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), 1L, "1", s);;
@ -137,11 +163,21 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
webSocketSenderApi.sendObject(UserTypeEnum.MEMBER.getValue(), userId, messageType, content);
}
@Async
public void sendAsyncMessageToMembers(Long userId, String messageType, Object content) {
webSocketSenderApi.sendObject(UserTypeEnum.MEMBER.getValue(), userId, messageType, content);
}
@Async
public void sendAsyncMessageToAdmin(String messageType, Object content) {
webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), messageType, content);
}
@Async
public void sendAsyncMessageToAdmins(String messageType, Object content) {
webSocketSenderApi.sendObject(UserTypeEnum.MEMBER.getValue(), messageType, content);
}
@Override
public PageResult<KeFuMessageDO> getKeFuMessagePage(KeFuMessagePageReqVO pageReqVO) {
return keFuMessageMapper.selectPage(pageReqVO);

View File

@ -27,4 +27,7 @@ public interface NotifyMessageSendApi {
*/
Long sendSingleMessageToMember(@Valid NotifySendSingleToUserReqDTO reqDTO);
}

View File

@ -40,10 +40,12 @@ public class DictDataController {
@Resource
private DictDataService dictDataService;
@Resource
public DictDataApi dictDataApi;
@PostMapping("/create")
@Operation(summary = "新增字典数据")
@PreAuthorize("@ss.hasPermission('system:dict:create')")
@ -159,6 +161,54 @@ public class DictDataController {
}
/**
* 修改装修商品分类字典数据
*/
@GetMapping(value = "/diy-template-goods")
public CommonResult<String> setGoods(String id) {
List<DictDataRespDTO> dictDataList = dictDataApi.getDictDataList("diy-template-goods");
DictDataRespDTO dictDataRespDTO = dictDataList.get(0);
DictDataSaveReqVO dictDataSaveReqVO = BeanUtils.toBean(dictDataRespDTO, DictDataSaveReqVO.class);
dictDataSaveReqVO.setValue(id);
dictDataService.updateDictData(dictDataSaveReqVO);
return success(id);
}
/**
* 修改装修商品分类字典数据
*/
@GetMapping(value = "/getGoods")
public CommonResult<String> getGoods() {
List<DictDataRespDTO> dictDataList = dictDataApi.getDictDataList("diy-template-goods");
DictDataRespDTO dictDataRespDTO = dictDataList.get(0);
return success(dictDataRespDTO.getValue());
}
/**
* 修改装修装修风格字典数据
*/
@GetMapping(value = "/diy-template-theme")
public CommonResult<String> setTheme(String id) {
List<DictDataRespDTO> dictDataList = dictDataApi.getDictDataList("diy-template-theme");
DictDataRespDTO dictDataRespDTO = dictDataList.get(0);
DictDataSaveReqVO dictDataSaveReqVO = BeanUtils.toBean(dictDataRespDTO, DictDataSaveReqVO.class);
dictDataSaveReqVO.setValue(id);
dictDataService.updateDictData(dictDataSaveReqVO);
return success(id);
}
/**
* 修改装修装修风格字典数据
*/
@GetMapping(value = "/getTheme")
public CommonResult<String> getTheme() {
List<DictDataRespDTO> dictDataList = dictDataApi.getDictDataList("diy-template-theme");
DictDataRespDTO dictDataRespDTO = dictDataList.get(0);
return success(dictDataRespDTO.getValue());
}
}

View File

@ -210,7 +210,7 @@ yudao:
websocket:
enable: true # websocket的开关
path: /infra/ws # 路径
sender-type: local # 消息发送的类型,可选值为 local、redis、rocketmq、kafka、rabbitmq
sender-type: redis # 消息发送的类型,可选值为 local、redis、rocketmq、kafka、rabbitmq
sender-rocketmq:
topic: ${spring.application.name}-websocket # 消息发送的 RocketMQ Topic
consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 RocketMQ Consumer Group