Compare commits

...

3 Commits

Author SHA1 Message Date
912e23ad44 Merge pull request 'sjy-three' (#118) from sjy-three into master
All checks were successful
continuous-integration/drone Build is passing
Reviewed-on: #118
2024-11-13 10:47:08 +08:00
5d5dadf0d5 添加客服配置 2024-11-13 10:43:51 +08:00
2fa158d3ff 添加客服配置 2024-11-13 10:39:51 +08:00
28 changed files with 876 additions and 117 deletions

View File

@ -0,0 +1,44 @@
import request from '@/config/axios'
// 客服配置 VO
export interface ConfigurationVO {
id: number // id
type: string // 客服类型
feedback: string // 客服反馈
phone: string // 客服电话
link: string // 客服链接
enterpriseID: string // 企业ID
}
// 客服配置 API
export const ConfigurationApi = {
// 查询客服配置分页
getConfigurationPage: async (params: any) => {
return await request.get({ url: `/promotion/ke-fu-configuration/page`, params })
},
// 查询客服配置详情
getConfiguration: async (id: number) => {
return await request.get({ url: `/promotion/ke-fu-configuration/get?id=` + id })
},
// 新增客服配置
createConfiguration: async (data: ConfigurationVO) => {
return await request.post({ url: `/promotion/ke-fu-configuration/create`, data })
},
// 修改客服配置
updateConfiguration: async (data: ConfigurationVO) => {
return await request.put({ url: `/promotion/ke-fu-configuration/update`, data })
},
// 删除客服配置
deleteConfiguration: async (id: number) => {
return await request.delete({ url: `/promotion/ke-fu-configuration/delete?id=` + id })
},
// 导出客服配置 Excel
exportConfiguration: async (params) => {
return await request.download({ url: `/promotion/ke-fu-configuration/export-excel`, params })
}
}

View File

@ -6,10 +6,11 @@
<div class="menu-area">
<el-button type="primary" plain @click="createType" style="width: 90;font-size: 12px;">
<Icon icon="ep:plus" class="mr-5px" /> 新增分类
<Icon icon="ep:plus" class="mr-5px"/>
新增分类
</el-button>
<el-menu :default-active="targetMenuId" style="width:182px">
<el-menu :default-active="targetMenuId" style="width:200px">
<el-menu-item :index="targetMenuId" :key="targetMenuId" @click="clickMenu(targetMenuId)">
全部类型
</el-menu-item>
@ -32,46 +33,54 @@
<!-- 内容区域 -->
<div class="content-wrap">
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<div style="margin-top: 25px;margin-left: 20px;">
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true"
label-width="68px">
<el-form-item label="文件类型" prop="type" width="80">
<el-input v-model="queryParams.type" placeholder="请输入文件类型" clearable
@keyup.enter="handleQuery"/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<!-- <el-form-item label="创建时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss"
type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" />
</el-form-item>
</el-form-item> -->
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
<Icon icon="ep:search" class="mr-5px"/>
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
<Icon icon="ep:refresh" class="mr-5px"/>
重置
</el-button>
<el-button type="primary" plain @click="openForm">
<Icon icon="ep:upload" class="mr-5px" /> 上传文件
<Icon icon="ep:upload" class="mr-5px"/>
上传文件
</el-button>
</el-form-item>
<span><img @click="liebiao" style="cursor: pointer;" class="mr-10px h-30px w-30px" src="@/assets/imgs/liebiao.png" /></span>
<span><img @click="tubiao" style="cursor: pointer;" class="mr-10px h-30px w-30px" src="@/assets/imgs/tubiao.png" /></span>
<span><img @click="liebiao" style="cursor: pointer;" class="mr-10px h-30px w-30px"
src="@/assets/imgs/liebiao.png"/></span>
<span><img @click="tubiao" style="cursor: pointer;" class="mr-10px h-30px w-30px"
src="@/assets/imgs/tubiao.png"/></span>
</el-form>
</ContentWrap>
</div>
<div style="margin-top: 10px;" >
<el-table v-loading="loading" :data="list" v-show="panduan == '1'">
<el-table-column label="文件内容" align="center" prop="url" width="110px">
<template #default="{ row }">
<el-image v-if="row.type.includes('image')" class="h-80px w-80px" lazy :src="row.url"
<el-image v-if="row.type.includes('image')" class="h-40px w-40px" lazy :src="row.url"
:preview-src-list="[row.url]" preview-teleported fit="cover"/>
<el-link v-else-if="row.type.includes('pdf')" type="primary" :href="row.url"
:underline="false" target="_blank">预览</el-link>
:underline="false" target="_blank">预览
</el-link>
<el-link v-else type="primary" download :href="row.url" :underline="false"
target="_blank">下载</el-link>
target="_blank">下载
</el-link>
</template>
</el-table-column>
<el-table-column label="文件名" align="center" prop="name" :show-overflow-tooltip="true"/>
@ -97,18 +106,34 @@
<div v-show="panduan == '2'">
<!-- 图片展示区域 -->
<div class="image-container">
<div v-for="item in tubiaoData" :key="item.id" class="image-item">
<div
v-for="item in tubiaoData"
:key="item.id"
class="image-item"
@mouseover="hover(item.id, true)"
@mouseleave="hover(item.id, false)"
>
<img :src="item.url" alt="图表"/>
<div class="image-name">{{ item.name }}</div>
<div v-show="hoveredIndex === item.id" class="button-container">
<button class="button">按钮1</button>
<button class="button">按钮2</button>
<button class="button">按钮3</button>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
<Pagination :total="total" v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"/>
</div>
</div>
<!-- 表单弹窗添加/修改 -->
@ -117,7 +142,8 @@
<UpdateForm ref="forRef" @success="getList"/>
<Dialog v-model="dialogVisibles" :title="dialogTitles">
<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="80px">
<el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules"
label-width="80px">
<el-form-item label="数据标签" prop="label">
<el-input v-model="formData.label" placeholder="请输入数据标签"/>
@ -158,6 +184,7 @@
import FileForm from './FileForm.vue'
import UpdateForm from './updateForm.vue'
import * as DictDataApi from '@/api/system/dict/dict.data'
const typeMenu = ref<DictDataApi.DictDataVO[]>([]) //
const targetMenuId = ref('0')
defineOptions({name: 'InfraFile'})
@ -204,6 +231,16 @@
})
const queryFormRef = ref() //
// const hoveredIndex = ref(null);
// const hover = (index: string, status: boolean) => {
// if (status) {
// hoveredIndex.value = index; //
// } else {
// hoveredIndex.value = ''; //
// }
// }
/** 查询列表 */
const getList = async () => {
loading.value = true
@ -217,6 +254,7 @@
loading.value = false
}
}
interface FileDataVO {
id: string
configId: string
@ -346,7 +384,6 @@
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
@ -357,7 +394,8 @@
message.success(t('common.delSuccess'))
//
await getList()
} catch { }
} catch {
}
}
/** 初始化 **/
@ -378,7 +416,7 @@
padding: 25px;
background-color: white;
width: 180px;
height: 1000px;
height: 700px;
/* 固定高度 */
overflow-y: hidden;
/* 禁止滚动 */
@ -387,9 +425,10 @@
}
.content-wrap {
background-color: white;
flex-grow: 1;
/* 使内容区域占据剩余空间 */
overflow-y: auto;
/* overflow-y: auto; */
/* 允许内容区域滚动 */
}
@ -407,14 +446,25 @@
/* 每张图片占用的宽度使每行显示5张 */
.image-item {
width: calc(20% - 8px); /* 宽度设为容器的20%,减去间距 */
margin-bottom: 16px;
}
/* 设置图片的最大宽度 */
.image-item img {
width: 100%;
height: auto;
border-radius: 8px; /* 图片边缘圆角(可选) */
width: 80%;
height: 80%;
/* border-radius: 8px; /* 图片边缘圆角(可选) */
border: 3px solid #f3f3f3;
}
.image-name {
margin-top: 5 px; /* 图片和名称之间的间距 */
font-size: 14px; /* 文字大小 */
color: #333; /* 文字颜色 */
word-wrap: break-word; /* 如果名称很长,自动换行 */
}
</style>

View File

@ -67,7 +67,7 @@
@click="openForm('create')"
v-hasPermi="['promotion:auto-response:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增自动
<Icon icon="ep:plus" class="mr-5px" /> 新增自动
</el-button>
<!-- <el-button
type="success"

View File

@ -0,0 +1,135 @@
<template>
<ContentWrap>
<span>客服配置</span>
<el-divider />
<div style="height:380px;width:650px;padding: 20px;">
<span>客服类型</span>
<span>
<el-radio-group v-model="radios" @change="handleRadioChange">
<el-radio value="1" size="large">系统客服</el-radio>
<el-radio value="2" size="large">拨打电话</el-radio>
<el-radio value="3" size="large">跳转链接</el-radio>
</el-radio-group>
</span>
<p style="margin-left: 80px;color:#bcbaba;font-size:13px">系统客服点击联系客服使用系统的自带客服拨打电话点击联系客服拨打客服电话跳转链接跳转外部链接联系客服</p>
<div v-show="radios == 1">
<span style="margin-top: 60px; vertical-align: middle;">客服反馈</span>
<span style="vertical-align: middle;">
<el-input
v-model="formData.feedback"
placeholder="请输入客服反馈"
style="width:550px"
:rows="5"
type="textarea"
/>
</span>
<p style="margin-left: 80px;color:#bcbaba;font-size:13px">暂无客服在线是联系客服跳转的客服反馈页面的显示文字</p>
</div>
<div v-show="radios == 2">
<span style="margin-top: 60px; vertical-align: middle;">客服电话</span>
<span style="vertical-align: middle;">
<el-input
v-model="formData.phone"
placeholder="请输入客服电话"
style="width:550px"
/>
</span>
<p style="margin-left: 80px;color:#bcbaba;font-size:13px">客服类型选择拨打电话时用户点击联系客服的联系电话</p>
</div>
<div v-show="radios == 3">
<span style="margin-top: 60px; vertical-align: middle;">客服链接</span>
<span style="vertical-align: middle;">
<el-input
v-model="formData.link"
placeholder="请输入客服链接"
style="width:550px"
/>
</span>
<p style="margin-left: 80px;color:#bcbaba;font-size:13px">客服类型选择跳转链接时跳转的链接地址</p>
<span style="margin-top: 60px; vertical-align: middle;">&nbsp;&nbsp;&nbsp;企业ID</span>
<span style="vertical-align: middle;">
<el-input
v-model="formData.enterpriseID"
placeholder="请输入企业ID"
style="width:550px"
/>
</span>
<p style="margin-left: 80px;color:#bcbaba;font-size:13px">如果客服链接填写企业微信客服小程序需要跳转企业微信客服的话需要配置此项并且在小程序客服中绑定企业ID</p>
</div>
<el-button style="margin-left: 80px;" type="primary" @click="submit">提交</el-button>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { ConfigurationApi, ConfigurationVO } from '@/api/mall/promotion/configuration'
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const radios = ref('1')
const formData = ref({
id: undefined,
type: undefined,
feedback: undefined,
phone: undefined,
link: undefined,
enterpriseID: undefined
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
formData.value = await ConfigurationApi.getConfiguration(Number(radios.value))
// radios.value = formData.value.id
console.log('11111',radios.value)
} finally {
loading.value = false
}
}
const handleRadioChange = async (value) =>{
formData.value = await ConfigurationApi.getConfiguration(value)
console.log("选中的值是: ", radios.value); //
}
const submit = async () => {
const data = formData.value as unknown as ConfigurationVO
await ConfigurationApi.updateConfiguration(data)
message.success('提交成功')
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -7,7 +7,7 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="分类" prop="type">
<el-form-item label="话术分类" prop="type">
<el-select v-model="formData.type" placeholder="请选择分类">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.KEFU_VERBAL_TRICK_TYPE)"
@ -17,10 +17,10 @@
/>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-form-item label="话术标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="详情" prop="details">
<el-form-item label="话术内容" prop="details">
<el-input v-model="formData.details" type="textarea" placeholder="请输入详情" :rows="6" />
</el-form-item>
</el-form>

View File

@ -6,7 +6,7 @@
<el-button type="primary" plain @click="createType" style="width: 90;font-size: 12px;">
<Icon icon="ep:plus" class="mr-5px" /> 新增分类
</el-button>
<el-menu :default-active="targetMenuId" style="width:183px">
<el-menu :default-active="targetMenuId" style="width:209px">
<el-menu-item v-for="item in huashuType" :index="item.value" :key="item.value"
@click="clickMenu(item.value)">{{item.label}}</el-menu-item>
</el-menu>
@ -14,9 +14,7 @@
<!-- 内容区域 -->
<div class="content-wrap">
<ContentWrap>
<div style="margin-top: 25px;margin-left:20px">
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<!-- <el-form-item label="分类" prop="type">
@ -51,10 +49,12 @@
</el-button> -->
</el-form-item>
</el-form>
</ContentWrap>
</div>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="ID" align="center" prop="id" />
<el-table-column label="分类" align="center" prop="type">
@ -80,7 +80,7 @@
</el-table-column>
</el-table>
</ContentWrap>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
@ -281,7 +281,7 @@
padding: 25px;
background-color: white;
width: 180px;
height: 1000px;
height: 700px;
/* 固定高度 */
overflow-y: hidden;
/* 禁止滚动 */
@ -293,6 +293,7 @@
flex-grow: 1;
/* 使内容区域占据剩余空间 */
overflow-y: auto;
background-color: #ffffff;
/* 允许内容区域滚动 */
}
</style>

View File

@ -76,7 +76,8 @@ public class SecurityFrameworkUtils {
if (authentication == null) {
return null;
}
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
LoginUser loginUser = authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
return loginUser;
}
/**

View File

@ -38,7 +38,12 @@ public abstract class AbstractWebSocketMessageSender implements WebSocketMessage
@Override
public void send(String sessionId, String messageType, String messageContent) {
send(sessionId, null, null, messageType, messageContent);
send(sessionId, (Integer) null,null, messageType, messageContent);
}
@Override
public void send(String sessionId, Long userId, Integer userType,String messageType, String messageContent) {
send(sessionId, userType,userId, messageType, messageContent);
}
/**

View File

@ -37,6 +37,11 @@ public interface WebSocketMessageSender {
*/
void send(String sessionId, String messageType, String messageContent);
void send(String sessionId, Long userId, Integer userType,String messageType, String messageContent);
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
}
@ -49,4 +54,8 @@ public interface WebSocketMessageSender {
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObject(String sessionId, Long userId, Integer userType,String messageType, String messageContent) {
send(sessionId,userId,userType, messageType, JsonUtils.toJsonString(messageContent));
}
}

View File

@ -1,10 +1,16 @@
package cn.iocoder.yudao.framework.websocket.core.sender.redis;
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.WebSocketSession;
import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
/**
* 基于 Redis {@link WebSocketMessageSender} 实现类
@ -16,6 +22,10 @@ public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender
private final RedisMQTemplate redisMQTemplate;
@Resource
private WebSocketSessionManager webSocketSessionManager;
public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager,
RedisMQTemplate redisMQTemplate) {
super(sessionManager);
@ -27,6 +37,7 @@ public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender
sendRedisMessage(null, userId, userType, messageType, messageContent);
}
//用户发
@Override
public void send(Integer userType, String messageType, String messageContent) {
sendRedisMessage(null, null, userType, messageType, messageContent);
@ -37,6 +48,11 @@ public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender
sendRedisMessage(sessionId, null, null, messageType, messageContent);
}
@Override
public void send(String sessionId, Long userId, Integer userType,String messageType, String messageContent) {
sendRedisMessage(sessionId, userId, userType, messageType, messageContent);
}
/**
* 通过 Redis 广播消息
*
@ -48,6 +64,25 @@ public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender
*/
private void sendRedisMessage(String sessionId, Long userId, Integer userType,
String messageType, String messageContent) {
if (userType == 2){
ConcurrentMap<String, WebSocketSession> stringWebSocketSessionConcurrentMap = webSocketSessionManager.idSessions();
for (Map.Entry<String, WebSocketSession> entry : stringWebSocketSessionConcurrentMap.entrySet()) {
String key = entry.getKey();
WebSocketSession session = entry.getValue();
Map<String, Object> attributes = session.getAttributes();
LoginUser loginUser = (LoginUser) attributes.get("LOGIN_USER");
if(loginUser.getId() == 1 && loginUser.getUserType() == 2){
sessionId = key;
break;
}
// 处理每个 WebSocketSession
System.out.println("Key: " + key + ", Session: " + session);
}
}
RedisWebSocketMessage mqMessage = new RedisWebSocketMessage()
.setSessionId(sessionId).setUserId(userId).setUserType(userType)
.setMessageType(messageType).setMessageContent(messageContent);

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.websocket.core.session;
import org.springframework.web.socket.WebSocketSession;
import java.util.Collection;
import java.util.concurrent.ConcurrentMap;
/**
* {@link WebSocketSession} 管理器的接口
@ -18,6 +19,7 @@ public interface WebSocketSessionManager {
*/
void addSession(WebSocketSession session);
ConcurrentMap<String, WebSocketSession> idSessions();
/**
* 移除 Session
*
@ -25,6 +27,8 @@ public interface WebSocketSessionManager {
*/
void removeSession(WebSocketSession session);
/**
* 获得指定编号的 Session
*

View File

@ -4,12 +4,12 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.WebSocketSession;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import javax.servlet.http.HttpSession;
import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
@ -19,6 +19,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
*
* @author 芋道源码
*/
@Service
public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
/**
@ -28,6 +29,8 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
*/
private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();
private HttpSession httpSession;
/**
* user WebSocketSession 映射
*
@ -39,6 +42,7 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
@Override
public void addSession(WebSocketSession session) {
// 添加到 idSessions
idSessions.put(session.getId(), session);
// 添加到 userSessions
@ -60,7 +64,18 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
sessions = userSessionsMap.get(user.getId());
}
}
System.out.println("---------sessions---------: " + session);
sessions.add(session);
String uri = session.getUri().toString();
System.out.println(uri);
Map<String, Object> attributes = session.getAttributes();
System.out.println("---------sessions---------: " + attributes);
}
@Override
public ConcurrentMap<String, WebSocketSession> idSessions() {
return idSessions;
}
@Override

View File

@ -39,6 +39,9 @@ public interface WebSocketSenderApi {
*/
void send(String sessionId, String messageType, String messageContent);
void send(String sessionId, Long userId, Integer userType,String messageType, String messageContent);
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
}
@ -51,4 +54,8 @@ public interface WebSocketSenderApi {
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObject(String sessionId, Long userId, Integer userType,String messageType, Object messageContent) {
send(sessionId,userId,userType, messageType, JsonUtils.toJsonString(messageContent));
}
}

View File

@ -31,4 +31,9 @@ public class WebSocketSenderApiImpl implements WebSocketSenderApi {
webSocketMessageSender.send(sessionId, messageType, messageContent);
}
@Override
public void send(String sessionId, Long userId, Integer userType, String messageType, String messageContent) {
webSocketMessageSender.send(sessionId, userId,userType,messageType, messageContent);
}
}

View File

@ -19,6 +19,8 @@ public interface ErrorCodeConstants {
// ========== 兑换记录 TODO 补充编号 ==========
ErrorCode POINT_ORDER_NOT_EXISTS = new ErrorCode(11111, "兑换记录不存在");
ErrorCode KE_FU_CONFIGURATION_NOT_EXISTS = new ErrorCode(22222222, "客服配置不存在");
// ========== Banner 相关 1-013-002-000 ============
ErrorCode BANNER_NOT_EXISTS = new ErrorCode(1_013_002_000, "Banner 不存在");

View File

@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.promotion.controller.admin.configuration;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import javax.validation.constraints.*;
import javax.validation.*;
import javax.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.promotion.controller.admin.configuration.vo.*;
import cn.iocoder.yudao.module.promotion.dal.dataobject.configuration.KeFuConfigurationDO;
import cn.iocoder.yudao.module.promotion.service.configuration.KeFuConfigurationService;
@Tag(name = "管理后台 - 客服配置")
@RestController
@RequestMapping("/promotion/ke-fu-configuration")
@Validated
public class KeFuConfigurationController {
@Resource
private KeFuConfigurationService keFuConfigurationService;
@PostMapping("/create")
@Operation(summary = "创建客服配置")
@PreAuthorize("@ss.hasPermission('promotion:ke-fu-configuration:create')")
public CommonResult<Long> createKeFuConfiguration(@Valid @RequestBody KeFuConfigurationSaveReqVO createReqVO) {
return success(keFuConfigurationService.createKeFuConfiguration(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新客服配置")
@PreAuthorize("@ss.hasPermission('promotion:ke-fu-configuration:update')")
public CommonResult<Boolean> updateKeFuConfiguration(@Valid @RequestBody KeFuConfigurationSaveReqVO updateReqVO) {
keFuConfigurationService.updateKeFuConfiguration(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除客服配置")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('promotion:ke-fu-configuration:delete')")
public CommonResult<Boolean> deleteKeFuConfiguration(@RequestParam("id") Long id) {
keFuConfigurationService.deleteKeFuConfiguration(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得客服配置")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('promotion:ke-fu-configuration:query')")
public CommonResult<KeFuConfigurationRespVO> getKeFuConfiguration(@RequestParam("id") Long id) {
KeFuConfigurationDO keFuConfiguration = keFuConfigurationService.getKeFuConfiguration(id);
return success(BeanUtils.toBean(keFuConfiguration, KeFuConfigurationRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得客服配置分页")
@PreAuthorize("@ss.hasPermission('promotion:ke-fu-configuration:query')")
public CommonResult<PageResult<KeFuConfigurationRespVO>> getKeFuConfigurationPage(@Valid KeFuConfigurationPageReqVO pageReqVO) {
PageResult<KeFuConfigurationDO> pageResult = keFuConfigurationService.getKeFuConfigurationPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, KeFuConfigurationRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出客服配置 Excel")
@PreAuthorize("@ss.hasPermission('promotion:ke-fu-configuration:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportKeFuConfigurationExcel(@Valid KeFuConfigurationPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<KeFuConfigurationDO> list = keFuConfigurationService.getKeFuConfigurationPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "客服配置.xls", "数据", KeFuConfigurationRespVO.class,
BeanUtils.toBean(list, KeFuConfigurationRespVO.class));
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.promotion.controller.admin.configuration.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 客服配置分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class KeFuConfigurationPageReqVO extends PageParam {
@Schema(description = "客服类型", example = "1")
private String type;
@Schema(description = "客服反馈")
private String feedback;
@Schema(description = "客服电话")
private String phone;
@Schema(description = "客服链接")
private String link;
@Schema(description = "企业ID", example = "20474")
private String enterpriseID;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.promotion.controller.admin.configuration.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;
@Schema(description = "管理后台 - 客服配置 Response VO")
@Data
@ExcelIgnoreUnannotated
public class KeFuConfigurationRespVO {
@Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "5189")
@ExcelProperty("id")
private Long id;
@Schema(description = "客服类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty("客服类型")
private String type;
@Schema(description = "客服反馈")
@ExcelProperty("客服反馈")
private String feedback;
@Schema(description = "客服电话")
@ExcelProperty("客服电话")
private String phone;
@Schema(description = "客服链接")
@ExcelProperty("客服链接")
private String link;
@Schema(description = "企业ID", example = "20474")
@ExcelProperty("企业ID")
private String enterpriseID;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.promotion.controller.admin.configuration.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import javax.validation.constraints.*;
@Schema(description = "管理后台 - 客服配置新增/修改 Request VO")
@Data
public class KeFuConfigurationSaveReqVO {
@Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "5189")
private Long id;
@Schema(description = "客服类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotEmpty(message = "客服类型不能为空")
private String type;
@Schema(description = "客服反馈")
private String feedback;
@Schema(description = "客服电话")
private String phone;
@Schema(description = "客服链接")
private String link;
@Schema(description = "企业ID", example = "20474")
private String enterpriseID;
}

View File

@ -42,4 +42,6 @@ public class KeFuMessageRespVO {
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
private String kefuName;
}

View File

@ -10,7 +10,11 @@ import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageRespVO;
import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessagePageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessageSendReqVO;
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.dataobject.supportstaff.SupportStaffDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuConversationMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.supportstaff.SupportStaffMapper;
import cn.iocoder.yudao.module.promotion.service.kefu.KeFuMessageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -33,9 +37,15 @@ public class AppKeFuMessageController {
@Resource
private KeFuMessageService kefuMessageService;
@Resource
KeFuConversationMapper keFuConversationMapper;
@Resource
private MemberUserApi memberUserApi;
@Resource
private SupportStaffMapper supportStaffMapper;
@PostMapping("/send")
@Operation(summary = "发送客服消息")
@ -70,11 +80,16 @@ public class AppKeFuMessageController {
String systemUserAvatar = kefuMessageService.findSystemUserAvatar(keFuMessageDO.getSenderId());
keFuMessageDO.setSenderAvatar(systemUserAvatar);
}
KeFuConversationDO user_di = keFuConversationMapper.selectOne("user_id", getLoginUserId());
SupportStaffDO supportStaffDO = supportStaffMapper.selectOne("id", user_di.getKefuId());
keFuMessageDO.setKefuName(supportStaffDO.getName());
}
return success(BeanUtils.toBean(pageResult, KeFuMessageRespVO.class));
}
@PostMapping("/test")
@Operation(summary = "获得客服消息分页")
@PreAuthenticated

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.configuration;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 客服配置 DO
*
* @author 管理员
*/
@TableName("promotion_kefu_configuration")
@KeySequence("promotion_kefu_configuration_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class KeFuConfigurationDO extends BaseDO {
/**
* id
*/
@TableId
private Long id;
/**
* 客服类型
*/
private String type;
/**
* 客服反馈
*/
private String feedback;
/**
* 客服电话
*/
private String phone;
/**
* 客服链接
*/
private String link;
/**
* 企业ID
*/
@TableField("enterpriseID")
private String enterpriseID;
}

View File

@ -82,4 +82,8 @@ public class KeFuMessageDO extends BaseDO {
@TableField(exist = false)
private String senderAvatar;
@TableField(exist = false)
private String kefuName;
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.configuration;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.promotion.dal.dataobject.configuration.KeFuConfigurationDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.promotion.controller.admin.configuration.vo.*;
/**
* 客服配置 Mapper
*
* @author 管理员
*/
@Mapper
public interface KeFuConfigurationMapper extends BaseMapperX<KeFuConfigurationDO> {
default PageResult<KeFuConfigurationDO> selectPage(KeFuConfigurationPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<KeFuConfigurationDO>()
.eqIfPresent(KeFuConfigurationDO::getType, reqVO.getType())
.eqIfPresent(KeFuConfigurationDO::getFeedback, reqVO.getFeedback())
.eqIfPresent(KeFuConfigurationDO::getPhone, reqVO.getPhone())
.eqIfPresent(KeFuConfigurationDO::getLink, reqVO.getLink())
.eqIfPresent(KeFuConfigurationDO::getEnterpriseID, reqVO.getEnterpriseID())
.betweenIfPresent(KeFuConfigurationDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(KeFuConfigurationDO::getId));
}
}

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.promotion.service.configuration;
import java.util.*;
import javax.validation.*;
import cn.iocoder.yudao.module.promotion.controller.admin.configuration.vo.*;
import cn.iocoder.yudao.module.promotion.dal.dataobject.configuration.KeFuConfigurationDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
/**
* 客服配置 Service 接口
*
* @author 管理员
*/
public interface KeFuConfigurationService {
/**
* 创建客服配置
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createKeFuConfiguration(@Valid KeFuConfigurationSaveReqVO createReqVO);
/**
* 更新客服配置
*
* @param updateReqVO 更新信息
*/
void updateKeFuConfiguration(@Valid KeFuConfigurationSaveReqVO updateReqVO);
/**
* 删除客服配置
*
* @param id 编号
*/
void deleteKeFuConfiguration(Long id);
/**
* 获得客服配置
*
* @param id 编号
* @return 客服配置
*/
KeFuConfigurationDO getKeFuConfiguration(Long id);
/**
* 获得客服配置分页
*
* @param pageReqVO 分页查询
* @return 客服配置分页
*/
PageResult<KeFuConfigurationDO> getKeFuConfigurationPage(KeFuConfigurationPageReqVO pageReqVO);
}

View File

@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.promotion.service.configuration;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import cn.iocoder.yudao.module.promotion.controller.admin.configuration.vo.*;
import cn.iocoder.yudao.module.promotion.dal.dataobject.configuration.KeFuConfigurationDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.dal.mysql.configuration.KeFuConfigurationMapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
/**
* 客服配置 Service 实现类
*
* @author 管理员
*/
@Service
@Validated
public class KeFuConfigurationServiceImpl implements KeFuConfigurationService {
@Resource
private KeFuConfigurationMapper keFuConfigurationMapper;
@Override
public Long createKeFuConfiguration(KeFuConfigurationSaveReqVO createReqVO) {
// 插入
KeFuConfigurationDO keFuConfiguration = BeanUtils.toBean(createReqVO, KeFuConfigurationDO.class);
keFuConfigurationMapper.insert(keFuConfiguration);
// 返回
return keFuConfiguration.getId();
}
@Override
public void updateKeFuConfiguration(KeFuConfigurationSaveReqVO updateReqVO) {
// 校验存在
validateKeFuConfigurationExists(updateReqVO.getId());
// 更新
KeFuConfigurationDO updateObj = BeanUtils.toBean(updateReqVO, KeFuConfigurationDO.class);
keFuConfigurationMapper.updateById(updateObj);
}
@Override
public void deleteKeFuConfiguration(Long id) {
// 校验存在
validateKeFuConfigurationExists(id);
// 删除
keFuConfigurationMapper.deleteById(id);
}
private void validateKeFuConfigurationExists(Long id) {
if (keFuConfigurationMapper.selectById(id) == null) {
throw exception(KE_FU_CONFIGURATION_NOT_EXISTS);
}
}
@Override
public KeFuConfigurationDO getKeFuConfiguration(Long id) {
return keFuConfigurationMapper.selectById(id);
}
@Override
public PageResult<KeFuConfigurationDO> getKeFuConfigurationPage(KeFuConfigurationPageReqVO pageReqVO) {
return keFuConfigurationMapper.selectPage(pageReqVO);
}
}

View File

@ -7,6 +7,7 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
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;
@ -26,8 +27,10 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@ -95,6 +98,7 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
getSelf().sendAsyncMessageToMembers(conversation.getKefuId(), KEFU_MESSAGE_TYPE, kefuMessage);
// 3. 通知所有管理员更新对话
getSelf().sendAsyncMessageToAdmins(KEFU_MESSAGE_TYPE, kefuMessage);
return kefuMessage.getId();
@ -165,7 +169,7 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
@Async
public void sendAsyncMessageToMembers(Long userId, String messageType, Object content) {
webSocketSenderApi.sendObject(UserTypeEnum.MEMBER.getValue(), userId, messageType, content);
webSocketSenderApi.sendObject("",userId, UserTypeEnum.ADMIN.getValue(), messageType, content);
}
@Async

View File

@ -225,9 +225,12 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
//获取用户头像和昵称
PayWalletDO payWalletDO = payWalletMapper.selectById(payWalletRechargeDO.getWalletId());
MemberUserRespDTO user = memberUserApi.getUser(payWalletDO.getUserId());
if(user.getLevelId() != null){
payWalletRechargeDO.setName(user.getNickname());
}
if (user.getAvatar() != null) {
payWalletRechargeDO.setAvatar(user.getAvatar());
}
}
return payWalletRechargeDOPageResult;