【新增】增加SSE消息推送模块,实现右上角站内信徽标数推送

【修复】修复个人中心-我的消息 关闭详情页是否已读未刷新的问题
pull/135/head
diant 2023-07-17 14:29:29 +08:00
parent 50c953709c
commit e931aec42b
22 changed files with 1038 additions and 24 deletions

View File

@ -33,6 +33,7 @@
"echarts": "5.4.0", "echarts": "5.4.0",
"echarts-stat": "1.2.0", "echarts-stat": "1.2.0",
"enquire.js": "2.1.6", "enquire.js": "2.1.6",
"event-source-polyfill": "^1.0.31",
"fuse.js": "6.6.2", "fuse.js": "6.6.2",
"highlight.js": "11.6.0", "highlight.js": "11.6.0",
"hotkeys-js": "3.10.1", "hotkeys-js": "3.10.1",

View File

@ -38,6 +38,10 @@ export default {
indexMessageDetail(data) { indexMessageDetail(data) {
return request('message/detail', data, 'get') return request('message/detail', data, 'get')
}, },
//站内信全部标记已读
indexMessageAllMarkRead(data) {
return request('message/allMessageMarkRead', data, 'get')
},
// 获取当前用户访问日志列表 // 获取当前用户访问日志列表
indexVisLogList(data) { indexVisLogList(data) {
return request('visLog/list', data, 'get') return request('visLog/list', data, 'get')

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="msg panel-item" :hidden="msgList.length == 0" @click="showMsg"> <div class="msg panel-item" @click="showMsg">
<a-badge :count="msgList.length" class="badge"> <a-badge :count="unreadMessageNum" class="badge">
<comment-outlined /> <comment-outlined />
</a-badge> </a-badge>
<a-drawer v-model:visible="msgVisible" title="新消息" placement="right"> <a-drawer v-model:visible="msgVisible" title="新消息" placement="right">
<a-list :data-source="msgList" size="small" class="mb-3"> <a-list :data-source="messageList" size="small" class="mb-3" :loading="miniMessageLoading">
<template #renderItem="{ item }"> <template #renderItem="{ item }">
<a-list-item> <a-list-item>
<a-list-item-meta :description="item.createTime"> <a-list-item-meta :description="item.createTime">
@ -16,35 +16,186 @@
</template> </template>
</a-list> </a-list>
<a-space> <a-space>
<a-button type="primary">消息中心</a-button> <a-button type="primary" @click="leaveFor('/usercenter')"></a-button>
<a-button @click="markRead"></a-button> <a-button @click="markRead"></a-button>
</a-space> </a-space>
</a-drawer> </a-drawer>
</div> </div>
<xn-form-container
title="详情"
:width="700"
:visible="visible"
:destroy-on-close="true"
@close="onClose"
>
<a-form ref="formRef" :model="formData" layout="vertical">
<a-form-item label="主题:" name="subject">
<span>{{ formData.subject }}</span>
</a-form-item>
<a-form-item label="发送时间:" name="createTime">
<span>{{ formData.createTime }}</span>
</a-form-item>
<a-form-item label="内容:" name="content">
<span>{{ formData.content }}</span>
</a-form-item>
<a-form-item label="查收情况:" name="receiveInfoList">
<s-table
ref="table"
:columns="columns"
:data="loadData"
:alert="false"
:showPagination="false"
bordered
:row-key="(record) => record.id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'read'">
<span v-if="record.read" style="color: #d9d9d9"></span>
<span v-else style="color: #ff4d4f">未读</span>
</template>
</template>
</s-table>
</a-form-item>
</a-form>
</xn-form-container>
</template> </template>
<script setup name="devUserMessage"> <script setup name="devUserMessage">
const msgVisible = ref(false) import indexApi from '@/api/sys/indexApi'
const msgList = ref([]) import router from '@/router'
import {onMounted} from "vue";
import sysConfig from "@/config";
import { EventSourcePolyfill } from "event-source-polyfill";
import tool from "@/utils/tool";
msgList.value = [ const miniMessageLoading = ref(false)
{ const msgVisible = ref(false)
subject: '凌晨发来一份电报', const messageList = ref([])
createTime: '2022-09-05 22:29:02' const unreadMessageNum = ref(0)
},
{ onMounted(() => {
subject: '听说2.0要发布了,是真的吗?', createSseConnect()
createTime: '2022-09-05 22:29:02' })
// sse
const createSseConnect = () => {
if (window.EventSource) {
let clientId = tool.data.get("CLIENTID") ? tool.data.get("CLIENTID") : "";
let url = sysConfig.API_URL+'/dev/message/createSseConnect?clientId='+clientId;
//heartbeatTimeout: 30s
let source = new EventSourcePolyfill(url,{headers: {token: tool.data.get('TOKEN')},heartbeatTimeout: 30000})
//
source.addEventListener('open', (e) => {
//console.log("",e)
})
//
source.addEventListener("message", (e) => {
const result = JSON.parse(e.data)
const code = result.code
const msg = result.msg
const data = result.data
if (code === 200) {
console.log("see推送消息:",data)
unreadMessageNum.value = data;
} else if (code === 0) {
// id
tool.data.set("CLIENTID", data)
console.log("客户端id:",data)
}
})
//
source.addEventListener("error", (e) => {
console.log("发生错误,已断开与服务器的连接:",e)
source.close();
})
} else {
console.log("该浏览器不支持sse")
} }
] }
//
const getMessageList = () => {
miniMessageLoading.value = true
indexApi
.indexMessageList()
.then((data) => {
messageList.value = data
})
.finally(() => {
miniMessageLoading.value = false
})
}
// //
const showMsg = () => { const showMsg = () => {
msgVisible.value = true msgVisible.value = true
getMessageList()
} }
// //
const markRead = () => { const markRead = () => {
msgList.value = [] messageList.value = []
unreadMessageNum.value = 0
indexApi.indexMessageAllMarkRead().then((data) => {})
}
//
const leaveFor = (url = '/') => {
msgVisible.value = false
router.replace({ path: url, query: { tab: 'userMessage' } })
}
//
const messageDetail = (message) => {
visible.value = true
const param = {
id: message.id
}
indexApi.indexMessageDetail(param).then((data) => {
Object.assign(message, data)
formData.value = message
receiveInfoList.value = data.receiveInfoList
table.value.refresh(true)
})
unreadMessageNum.value = Math.max(unreadMessageNum.value - 1, 0);
}
const loadData = () => {
return new Promise((resolve) => {
resolve(receiveInfoList.value)
})
}
//
const visible = ref(false)
const formRef = ref()
const receiveInfoList = ref([])
const formData = ref({})
const table = ref()
const columns = [
{
title: '姓名',
dataIndex: 'receiveUserName'
},
{
title: '是否已读',
dataIndex: 'read',
width: 120
}
]
//
const onClose = () => {
visible.value = false
formData.value = {}
receiveInfoList.value = []
} }
</script> </script>
<style scoped></style> <style scoped>
/deep/ .ant-badge-count{
padding: 0px;
min-width: 15px;
height: 15px;
line-height: 15px;
}
</style>

View File

@ -6,7 +6,7 @@
<div v-if="!ismobile" class="screen panel-item hidden-sm-and-down" @click="fullscreen"> <div v-if="!ismobile" class="screen panel-item hidden-sm-and-down" @click="fullscreen">
<fullscreen-outlined /> <fullscreen-outlined />
</div> </div>
<!--<devUserMessage />--> <devUserMessage />
<a-dropdown class="user panel-item"> <a-dropdown class="user panel-item">
<div class="user-avatar"> <div class="user-avatar">
<a-avatar :src="userInfo.avatar" /> <a-avatar :src="userInfo.avatar" />

View File

@ -30,7 +30,7 @@
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
<detail ref="detailRef" /> <detail ref="detailRef" @refresh="refresh"/>
</template> </template>
<script setup name="userMessage"> <script setup name="userMessage">
@ -70,6 +70,9 @@
return data return data
}) })
} }
const refresh = () => {
table.value.refresh(false)
}
// //
const handleClick = () => { const handleClick = () => {
// //

View File

@ -32,6 +32,7 @@
<script setup name="messageDetail"> <script setup name="messageDetail">
import userCenterApi from '@/api/sys/userCenterApi' import userCenterApi from '@/api/sys/userCenterApi'
const emits = defineEmits(["refresh"]);
const receiveInfoList = ref([]) const receiveInfoList = ref([])
// //
@ -77,6 +78,7 @@
const onClose = () => { const onClose = () => {
receiveInfoList.value = [] receiveInfoList.value = []
visible = false visible = false
emits("refresh");
} }
// //
defineExpose({ defineExpose({

View File

@ -0,0 +1,31 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.common.sse;
import lombok.Getter;
import lombok.Setter;
/**
* SSE
*
* @author diantu
* @date 2023/7/10
*/
@Getter
@Setter
public class CommonSseParam {
private String clientId;
private String loginId;
}

View File

@ -75,6 +75,14 @@ public interface DevMessageApi {
*/ */
List<JSONObject> list(List<String> receiverIdList, Integer limit); List<JSONObject> list(List<String> receiverIdList, Integer limit);
/**
*
*
* @author diantu
* @date 2023/7/10
*/
Long unreadCount(String loginId);
/** /**
* *
* *
@ -92,4 +100,12 @@ public interface DevMessageApi {
*/ */
JSONObject detail(String id); JSONObject detail(String id);
/**
*
*
* @author diantu
* @date 2023/7/10
*/
void allMessageMarkRead();
} }

View File

@ -0,0 +1,67 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.api;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.common.sse.CommonSseParam;
import java.util.function.Consumer;
/**
* SSE API
*
* @author diantu
* @date 2023/7/5
**/
public interface DevSseApi {
/**
* SSE
*
* @param clientId id,
* @param setHeartBeat ,falsetrue: false:
* @param consumer ,ConsumeracceptsetHeartBeattrue
* @return id,0
* @author diantu
* @date 2023/7/5
**/
public SseEmitter createSseConnect(String clientId, Boolean setHeartBeat, Consumer<CommonSseParam> consumer);
/**
*
*
* @param clientId id
* @author diantu
* @date 2023/7/5
**/
public void closeSseConnect(String clientId);
/**
*
*
* @param msg
* @author diantu
* @date 2023/7/5
**/
public void sendMessageToAllClient(String msg);
/**
* clientId
*
* @param clientId id
* @param msg
* @author diantu
* @date 2023/7/5
**/
public void sendMessageToOneClient(String clientId, String msg);
}

View File

@ -12,9 +12,11 @@
*/ */
package vip.xiaonuo.dev.modular.message.provider; package vip.xiaonuo.dev.modular.message.provider;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import vip.xiaonuo.dev.api.DevMessageApi; import vip.xiaonuo.dev.api.DevMessageApi;
@ -22,6 +24,9 @@ import vip.xiaonuo.dev.modular.message.param.DevMessageIdParam;
import vip.xiaonuo.dev.modular.message.param.DevMessageListParam; import vip.xiaonuo.dev.modular.message.param.DevMessageListParam;
import vip.xiaonuo.dev.modular.message.param.DevMessageSendParam; import vip.xiaonuo.dev.modular.message.param.DevMessageSendParam;
import vip.xiaonuo.dev.modular.message.service.DevMessageService; import vip.xiaonuo.dev.modular.message.service.DevMessageService;
import vip.xiaonuo.dev.modular.relation.entity.DevRelation;
import vip.xiaonuo.dev.modular.relation.enums.DevRelationCategoryEnum;
import vip.xiaonuo.dev.modular.relation.service.DevRelationService;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; import java.util.List;
@ -39,6 +44,9 @@ public class DevMessageProvider implements DevMessageApi {
@Resource @Resource
private DevMessageService devMessageService; private DevMessageService devMessageService;
@Resource
private DevRelationService devRelationService;
@Override @Override
public void sendMessage(List<String> receiverIdList, String subject) { public void sendMessage(List<String> receiverIdList, String subject) {
this.sendMessage(receiverIdList, subject, null); this.sendMessage(receiverIdList, subject, null);
@ -72,6 +80,11 @@ public class DevMessageProvider implements DevMessageApi {
return devMessageService.list(devMessageListParam).stream().map(JSONUtil::parseObj).collect(Collectors.toList()); return devMessageService.list(devMessageListParam).stream().map(JSONUtil::parseObj).collect(Collectors.toList());
} }
@Override
public Long unreadCount(String loginId){
return devMessageService.unreadCount(loginId);
}
@Override @Override
public Page<JSONObject> page(List<String> receiverIdList, String category) { public Page<JSONObject> page(List<String> receiverIdList, String category) {
return devMessageService.page(receiverIdList, category); return devMessageService.page(receiverIdList, category);
@ -83,4 +96,14 @@ public class DevMessageProvider implements DevMessageApi {
devMessageIdParam.setId(id); devMessageIdParam.setId(id);
return JSONUtil.parseObj(devMessageService.detail(devMessageIdParam)); return JSONUtil.parseObj(devMessageService.detail(devMessageIdParam));
} }
@Override
public void allMessageMarkRead(){
// 设置为已读
String myMessageExtJson = "{\"read\":true}";
devRelationService.update(new LambdaUpdateWrapper<DevRelation>()
.eq(DevRelation::getTargetId, StpUtil.getLoginIdAsString())
.eq(DevRelation::getCategory, DevRelationCategoryEnum.MSG_TO_USER.getValue())
.set(DevRelation::getExtJson, myMessageExtJson));
}
} }

View File

@ -64,6 +64,14 @@ public interface DevMessageService extends IService<DevMessage> {
*/ */
List<DevMessage> list(DevMessageListParam devMessageListParam); List<DevMessage> list(DevMessageListParam devMessageListParam);
/**
*
*
* @author diantu
* @date 2023/7/10
*/
Long unreadCount(String loginId);
/** /**
* *
* *

View File

@ -44,7 +44,6 @@ import vip.xiaonuo.dev.modular.relation.entity.DevRelation;
import vip.xiaonuo.dev.modular.relation.enums.DevRelationCategoryEnum; import vip.xiaonuo.dev.modular.relation.enums.DevRelationCategoryEnum;
import vip.xiaonuo.dev.modular.relation.service.DevRelationService; import vip.xiaonuo.dev.modular.relation.service.DevRelationService;
import vip.xiaonuo.sys.api.SysUserApi; import vip.xiaonuo.sys.api.SysUserApi;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@ -65,6 +64,7 @@ public class DevMessageServiceImpl extends ServiceImpl<DevMessageMapper, DevMess
@Resource @Resource
private DevRelationService devRelationService; private DevRelationService devRelationService;
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public void send(DevMessageSendParam devMessageSendParam) { public void send(DevMessageSendParam devMessageSendParam) {
@ -152,6 +152,13 @@ public class DevMessageServiceImpl extends ServiceImpl<DevMessageMapper, DevMess
return CollectionUtil.newArrayList(); return CollectionUtil.newArrayList();
} }
@Override
public Long unreadCount(String loginId){
return devRelationService.getRelationListByTargetIdAndCategory(loginId,
DevRelationCategoryEnum.MSG_TO_USER.getValue()).stream().filter(devRelation -> JSONUtil
.parseObj(devRelation.getExtJson()).getBool("read").equals(false)).count();
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public void delete(List<DevMessageIdParam> devMessageIdParamList) { public void delete(List<DevMessageIdParam> devMessageIdParamList) {
@ -206,4 +213,5 @@ public class DevMessageServiceImpl extends ServiceImpl<DevMessageMapper, DevMess
} }
return devMessage; return devMessage;
} }
} }

View File

@ -32,7 +32,7 @@ import javax.annotation.Resource;
* @date 2022/6/21 14:57 * @date 2022/6/21 14:57
**/ **/
@Api(tags = "监控控制器") @Api(tags = "监控控制器")
@ApiSupport(author = "SNOWY_TEAM", order = 8) @ApiSupport(author = "SNOWY_TEAM", order = 9)
@RestController @RestController
@Validated @Validated
public class DevMonitorController { public class DevMonitorController {

View File

@ -0,0 +1,95 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.modular.sse.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.common.sse.CommonSseParam;
import vip.xiaonuo.dev.modular.sse.service.DevSseEmitterService;
import javax.annotation.Resource;
import java.util.function.Consumer;
/**
* SSE
*
* @author diantu
* @date 2023/7/3
**/
@Api(tags = "SSE通信控制器")
@ApiSupport(author = "SNOWY_TEAM", order = 10)
@RestController
@Validated
public class DevSseEmitterController {
@Resource
private DevSseEmitterService devSseEmitterService;
/**
* sse
*
* @author diantu
* @date 2023/7/3
**/
@ApiOperationSupport(order = 1)
@ApiOperation("创建sse连接")
@GetMapping("/dev/sse/createConnect")
public SseEmitter createConnect(String clientId, @RequestParam(required = false)Boolean setHeartBeat,
@RequestParam(required = false)Consumer<CommonSseParam> consumer){
return devSseEmitterService.createSseConnect(clientId,setHeartBeat,consumer);
}
/**
* sse
*
* @author diantu
* @date 2023/7/3
**/
@ApiOperationSupport(order = 2)
@ApiOperation("关闭sse连接")
@GetMapping("/dev/sse/closeSseConnect")
public void closeSseConnect(String clientId){
devSseEmitterService.closeSseConnect(clientId);
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
@ApiOperationSupport(order = 3)
@ApiOperation("推送消息到所有客户端")
@PostMapping("/dev/sse/broadcast")
public void sendMessageToAllClient(@RequestBody(required = false) String msg){
devSseEmitterService.sendMessageToAllClient(msg);
}
/**
* clientId
*
* @author diantu
* @date 2023/7/3
**/
@ApiOperationSupport(order = 4)
@ApiOperation("根据clientId发送消息给某一客户端")
@PostMapping("/dev/sse/sendMessage")
public void sendMessageToOneClient(String clientId,String msg){
devSseEmitterService.sendMessageToOneClient(clientId,msg);
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.modular.sse.enums;
import lombok.Getter;
/**
* SSE
*
* @author diantu
* @date 2023/7/17
**/
@Getter
public enum DevSseEmitterParameterEnum {
/**
*
*/
EMITTER("EMITTER"),
/**
*
*/
FUTURE("FUTURE"),
/**
* ID
*/
LOGINID("LOGINID");
private final String value;
DevSseEmitterParameterEnum(String value) {
this.value = value;
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.modular.sse.provider;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.common.sse.CommonSseParam;
import vip.xiaonuo.dev.api.DevSseApi;
import vip.xiaonuo.dev.modular.sse.service.DevSseEmitterService;
import javax.annotation.Resource;
import java.util.function.Consumer;
/**
* SSE API
*
* @author diantu
* @date 2023/7/5
**/
@Service
public class DevSseProvider implements DevSseApi {
@Resource
private DevSseEmitterService devSseEmitterService;
/**
* SSE
*
* @author diantu
* @date 2023/7/5
**/
@Override
public SseEmitter createSseConnect(String clientId, Boolean setHeartBeat, Consumer<CommonSseParam> consumer) {
return devSseEmitterService.createSseConnect(clientId,setHeartBeat,consumer);
}
/**
*
*
* @author diantu
* @date 2023/7/5
**/
@Override
public void closeSseConnect(String clientId) {
devSseEmitterService.closeSseConnect(clientId);
}
/**
*
*
* @author diantu
* @date 2023/7/5
**/
@Override
public void sendMessageToAllClient(String msg) {
devSseEmitterService.sendMessageToAllClient(msg);
}
/**
* clientId
*
* @author diantu
* @date 2023/7/5
**/
@Override
public void sendMessageToOneClient(String clientId, String msg) {
devSseEmitterService.sendMessageToOneClient(clientId,msg);
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.modular.sse.service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.common.sse.CommonSseParam;
import java.util.function.Consumer;
/**
* SSEService
*
* @author diantu
* @date 2023/7/3
**/
public interface DevSseEmitterService {
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public SseEmitter createSseConnect(String clientId,Boolean setHeartBeat, Consumer<CommonSseParam> consumer);
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public void closeSseConnect(String clientId);
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public void sendMessageToAllClient(String msg);
/**
* clientId
*
* @author diantu
* @date 2023/7/3
**/
public void sendMessageToOneClient(String clientId, String msg);
}

View File

@ -0,0 +1,124 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.modular.sse.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.common.pojo.CommonResult;
import vip.xiaonuo.common.sse.CommonSseParam;
import vip.xiaonuo.dev.modular.sse.service.DevSseEmitterService;
import vip.xiaonuo.dev.modular.sse.util.DevSseCacheUtil;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* SSEService
*
* @author diantu
* @date 2023/7/3
**/
@Slf4j
@Service
public class DevSseEmitterServiceImpl implements DevSseEmitterService {
/**
* 线
*/
private static final ScheduledExecutorService heartbeatExecutors = Executors.newScheduledThreadPool(10);
/**
*
*
* @author diantu
* @date 2023/7/3
**/
@Override
public SseEmitter createSseConnect(String clientId, Boolean setHeartBeat, Consumer<CommonSseParam> consumer) {
// 设置超时时间0表示不过期。默认30秒超过时间未完成会抛出异常AsyncRequestTimeoutException
SseEmitter sseEmitter = new SseEmitter(0L);
String loginId = StpUtil.getLoginIdAsString();
// 判断连接是否有效
if (DevSseCacheUtil.connectionValidity(clientId,loginId)) {
return DevSseCacheUtil.getSseEmitterByClientId(clientId);
}else{
DevSseCacheUtil.removeConnection(clientId);
}
clientId = IdUtil.simpleUUID();
String finalClientId = clientId;
// 增加心跳
final ScheduledFuture<?> future;
// 是否自定义心跳任务
if (setHeartBeat!=null&&setHeartBeat) {
CommonSseParam commonSseParam = new CommonSseParam();
commonSseParam.setClientId(clientId);
commonSseParam.setLoginId(loginId);
future = heartbeatExecutors.scheduleAtFixedRate(() -> consumer.accept(commonSseParam),
2, 10, TimeUnit.SECONDS);
} else {
//默认心跳任务
future = heartbeatExecutors.scheduleAtFixedRate(() ->
DevSseCacheUtil.sendMessageToOneClient(finalClientId,finalClientId+"-"+loginId),
2, 10, TimeUnit.SECONDS);
}
// 长链接完成后回调(即关闭连接时调用)
sseEmitter.onCompletion(DevSseCacheUtil.completionCallBack(clientId,future));
// 连接超时回调
sseEmitter.onTimeout(DevSseCacheUtil.timeoutCallBack(clientId,future));
// 推送消息异常回调
sseEmitter.onError(DevSseCacheUtil.errorCallBack(clientId,future));
// 增加连接
DevSseCacheUtil.addConnection(clientId, loginId, sseEmitter, future);
// 初次建立连接,推送客户端id
CommonResult<String> message = new CommonResult<>(0,"",clientId);
DevSseCacheUtil.sendMessageToClientByClientId(clientId,message);
return sseEmitter;
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
@Override
public void closeSseConnect(String clientId){
DevSseCacheUtil.removeConnection(clientId);
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
@Override
public void sendMessageToAllClient(String msg) {
DevSseCacheUtil.sendMessageToAllClient(msg);
}
/**
* clientId
*
* @author diantu
* @date 2023/7/3
**/
@Override
public void sendMessageToOneClient(String clientId, String msg) {
DevSseCacheUtil.sendMessageToOneClient(clientId,msg);
}
}

View File

@ -0,0 +1,231 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* SnowyAPACHE LICENSE 2.0使
*
* 1.LICENSE
* 2.Snowy
* 3.使使
* 4. https://www.xiaonuo.vip
* 5.xiaonuobase@qq.com
* 6.Snowy https://www.xiaonuo.vip
*/
package vip.xiaonuo.dev.modular.sse.util;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.common.exception.CommonException;
import vip.xiaonuo.common.pojo.CommonResult;
import vip.xiaonuo.dev.modular.sse.enums.DevSseEmitterParameterEnum;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.function.Consumer;
/**
* SseEmitter
*
* @author diantu
* @date 2023/7/3
**/
@Slf4j
public class DevSseCacheUtil {
/**
* SseEmitter(使ConcurrentHashMap线)
*/
public static Map<String, Map<String,Object>> sseCache = new ConcurrentHashMap<>();
/**
* id
*
* @author diantu
* @date 2023/7/3
**/
public static SseEmitter getSseEmitterByClientId(String clientId) {
Map<String,Object> map = sseCache.get(clientId);
if (map == null || map.isEmpty()) {
return null;
}
return (SseEmitter) map.get(DevSseEmitterParameterEnum.EMITTER.getValue());
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static boolean existSseCache() {
return sseCache.size()>0;
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static boolean connectionValidity(String clientId,String loginId){
if(sseCache.get(clientId) == null){
return false;
}
return Objects.equals(loginId, sseCache.get(clientId).get(DevSseEmitterParameterEnum.LOGINID.getValue()));
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static void addConnection(String clientId,String loginId, SseEmitter emitter, ScheduledFuture<?> future) {
final SseEmitter oldEmitter = getSseEmitterByClientId(clientId);
if (oldEmitter != null) {
throw new CommonException("连接已存在:{}",clientId);
}
Map<String,Object> map = new ConcurrentHashMap<>();
map.put(DevSseEmitterParameterEnum.EMITTER.getValue(),emitter);
map.put(DevSseEmitterParameterEnum.FUTURE.getValue(), future);
map.put(DevSseEmitterParameterEnum.LOGINID.getValue(), loginId);
sseCache.put(clientId, map);
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static void removeConnection(String clientId) {
SseEmitter emitter = getSseEmitterByClientId(clientId);
if (emitter != null) {
cancelScheduledFuture((ScheduledFuture<?>) sseCache.get(clientId).get(DevSseEmitterParameterEnum.FUTURE.getValue()));
}
sseCache.remove(clientId);
log.info("移除连接:{}", clientId);
}
/**
*
*
* @author diantu
* @date 2023/7/3
*/
public static void cancelScheduledFuture(ScheduledFuture<?> future){
if (future != null) {
future.cancel(true);
}
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static Runnable completionCallBack(String clientId, ScheduledFuture<?> future) {
return () -> {
log.info("结束连接:{}", clientId);
removeConnection(clientId);
cancelScheduledFuture(future);
};
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static Runnable timeoutCallBack(String clientId, ScheduledFuture<?> future){
return ()->{
log.info("连接超时:{}", clientId);
removeConnection(clientId);
cancelScheduledFuture(future);
};
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static Consumer<Throwable> errorCallBack(String clientId, ScheduledFuture<?> future) {
return throwable -> {
log.info("推送消息异常:{}", clientId);
removeConnection(clientId);
cancelScheduledFuture(future);
};
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static void sendMessageToAllClient(String msg) {
if (!existSseCache()) {
return;
}
// 判断发送的消息是否为空
if (StrUtil.isEmpty(msg)){
log.info("群发消息为空");
return;
}
CommonResult<String> message = new CommonResult<>(CommonResult.CODE_SUCCESS,"",msg);
for (Map.Entry<String, Map<String, Object>> entry : sseCache.entrySet()) {
sendMessageToClientByClientId(entry.getKey(), message);
}
}
/**
* clientId
*
* @author diantu
* @date 2023/7/3
**/
public static void sendMessageToOneClient(String clientId, String msg) {
if (StrUtil.isEmpty(clientId)){
log.info("客户端ID为空");
return;
}
if (StrUtil.isEmpty(msg)){
log.info("向客户端{}推送消息为空",clientId);
return;
}
CommonResult<String> message = new CommonResult<>(CommonResult.CODE_SUCCESS,"",msg);
sendMessageToClientByClientId(clientId,message);
}
/**
*
*
* @author diantu
* @date 2023/7/3
**/
public static void sendMessageToClientByClientId(String clientId, CommonResult<String> message) {
Map<String, Object> map = sseCache.get(clientId);
if (map==null||map.size()==0) {
log.error("推送消息失败:客户端{}未创建长链接,失败消息:{}",clientId, message.toString());
return;
}
SseEmitter.SseEventBuilder sendData = SseEmitter.event().data(message,MediaType.APPLICATION_JSON);
SseEmitter sseEmitter = getSseEmitterByClientId(clientId);
try {
Objects.requireNonNull(sseEmitter).send(sendData);
} catch (Exception e) {
log.error("推送消息失败,报错异常:",e);
removeConnection(clientId);
}
}
}

View File

@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.common.annotation.CommonLog; import vip.xiaonuo.common.annotation.CommonLog;
import vip.xiaonuo.common.pojo.CommonResult; import vip.xiaonuo.common.pojo.CommonResult;
import vip.xiaonuo.common.pojo.CommonValidList; import vip.xiaonuo.common.pojo.CommonValidList;
@ -118,13 +119,27 @@ public class SysIndexController {
return CommonResult.data(sysIndexService.messageDetail(sysIndexMessageIdParam)); return CommonResult.data(sysIndexService.messageDetail(sysIndexMessageIdParam));
} }
/**
*
*
* @author diantu
* @date 2023/7/10
*/
@ApiOperationSupport(order = 6)
@ApiOperation("站内信全部标记已读")
@GetMapping("/dev/message/allMessageMarkRead")
public CommonResult<String> allMessageMarkRead() {
sysIndexService.allMessageMarkRead();
return CommonResult.ok();
}
/** /**
* 访 * 访
* *
* @author xuyuxiang * @author xuyuxiang
* @date 2022/4/24 20:00 * @date 2022/4/24 20:00
*/ */
@ApiOperationSupport(order = 6) @ApiOperationSupport(order = 7)
@ApiOperation("获取当前用户访问日志列表") @ApiOperation("获取当前用户访问日志列表")
@GetMapping("/sys/index/visLog/list") @GetMapping("/sys/index/visLog/list")
public CommonResult<List<SysIndexVisLogListResult>> visLogList() { public CommonResult<List<SysIndexVisLogListResult>> visLogList() {
@ -137,10 +152,23 @@ public class SysIndexController {
* @author xuyuxiang * @author xuyuxiang
* @date 2022/4/24 20:00 * @date 2022/4/24 20:00
*/ */
@ApiOperationSupport(order = 7) @ApiOperationSupport(order = 8)
@ApiOperation("获取当前用户操作日志列表") @ApiOperation("获取当前用户操作日志列表")
@GetMapping("/sys/index/opLog/list") @GetMapping("/sys/index/opLog/list")
public CommonResult<List<SysIndexOpLogListResult>> opLogList() { public CommonResult<List<SysIndexOpLogListResult>> opLogList() {
return CommonResult.data(sysIndexService.opLogList()); return CommonResult.data(sysIndexService.opLogList());
} }
/**
* sse
*
* @author diantu
* @date 2023/7/10
**/
@ApiOperationSupport(order = 9)
@ApiOperation("创建sse连接")
@GetMapping("/dev/message/createSseConnect")
public SseEmitter createSseConnect(String clientId){
return sysIndexService.createSseConnect(clientId);
}
} }

View File

@ -12,6 +12,7 @@
*/ */
package vip.xiaonuo.sys.modular.index.service; package vip.xiaonuo.sys.modular.index.service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.common.pojo.CommonValidList; import vip.xiaonuo.common.pojo.CommonValidList;
import vip.xiaonuo.sys.modular.index.param.*; import vip.xiaonuo.sys.modular.index.param.*;
import vip.xiaonuo.sys.modular.index.result.*; import vip.xiaonuo.sys.modular.index.result.*;
@ -66,6 +67,14 @@ public interface SysIndexService {
*/ */
SysIndexMessageDetailResult messageDetail(SysIndexMessageIdParam sysIndexMessageIdParam); SysIndexMessageDetailResult messageDetail(SysIndexMessageIdParam sysIndexMessageIdParam);
/**
*
*
* @author diantu
* @date 2023/7/10
*/
void allMessageMarkRead();
/** /**
* 访 * 访
* *
@ -81,4 +90,12 @@ public interface SysIndexService {
* @date 2022/9/4 15:11 * @date 2022/9/4 15:11
*/ */
List<SysIndexOpLogListResult> opLogList(); List<SysIndexOpLogListResult> opLogList();
/**
*
*
* @author diantu
* @date 2023/7/10
**/
public SseEmitter createSseConnect(String clientId);
} }

View File

@ -18,20 +18,23 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser; import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser;
import vip.xiaonuo.auth.core.util.StpLoginUserUtil; import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
import vip.xiaonuo.common.pojo.CommonValidList; import vip.xiaonuo.common.pojo.CommonValidList;
import vip.xiaonuo.common.sse.CommonSseParam;
import vip.xiaonuo.dev.api.DevLogApi; import vip.xiaonuo.dev.api.DevLogApi;
import vip.xiaonuo.dev.api.DevMessageApi; import vip.xiaonuo.dev.api.DevMessageApi;
import vip.xiaonuo.dev.api.DevSseApi;
import vip.xiaonuo.sys.modular.index.param.*; import vip.xiaonuo.sys.modular.index.param.*;
import vip.xiaonuo.sys.modular.index.result.*; import vip.xiaonuo.sys.modular.index.result.*;
import vip.xiaonuo.sys.modular.index.service.SysIndexService; import vip.xiaonuo.sys.modular.index.service.SysIndexService;
import vip.xiaonuo.sys.modular.relation.entity.SysRelation; import vip.xiaonuo.sys.modular.relation.entity.SysRelation;
import vip.xiaonuo.sys.modular.relation.enums.SysRelationCategoryEnum; import vip.xiaonuo.sys.modular.relation.enums.SysRelationCategoryEnum;
import vip.xiaonuo.sys.modular.relation.service.SysRelationService; import vip.xiaonuo.sys.modular.relation.service.SysRelationService;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -52,6 +55,9 @@ public class SysIndexServiceImpl implements SysIndexService {
@Resource @Resource
private DevLogApi devLogApi; private DevLogApi devLogApi;
@Resource
private DevSseApi devSseApi;
@Override @Override
public void addSchedule(SysIndexScheduleAddParam sysIndexScheduleAddParam) { public void addSchedule(SysIndexScheduleAddParam sysIndexScheduleAddParam) {
SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser(); SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
@ -94,6 +100,11 @@ public class SysIndexServiceImpl implements SysIndexService {
return JSONUtil.toBean(devMessageApi.detail(sysIndexMessageIdParam.getId()), SysIndexMessageDetailResult.class); return JSONUtil.toBean(devMessageApi.detail(sysIndexMessageIdParam.getId()), SysIndexMessageDetailResult.class);
} }
@Override
public void allMessageMarkRead(){
devMessageApi.allMessageMarkRead();
}
@Override @Override
public List<SysIndexVisLogListResult> visLogList() { public List<SysIndexVisLogListResult> visLogList() {
return devLogApi.currentUserVisLogList().stream() return devLogApi.currentUserVisLogList().stream()
@ -105,4 +116,15 @@ public class SysIndexServiceImpl implements SysIndexService {
return devLogApi.currentUserOpLogList().stream() return devLogApi.currentUserOpLogList().stream()
.map(jsonObject -> JSONUtil.toBean(jsonObject, SysIndexOpLogListResult.class)).collect(Collectors.toList()); .map(jsonObject -> JSONUtil.toBean(jsonObject, SysIndexOpLogListResult.class)).collect(Collectors.toList());
} }
@Override
public SseEmitter createSseConnect(String clientId){
Consumer<CommonSseParam> consumer = m -> {
//获取用户未读消息
long unreadMessageNum = devMessageApi.unreadCount(m.getLoginId());
//发送消息
devSseApi.sendMessageToOneClient(m.getClientId(), String.valueOf(unreadMessageNum));
};
return devSseApi.createSseConnect(clientId,true,consumer);
}
} }