提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
1. 我的消息功能
1.1 业务分析
这个是站内通信
我的消息功能
站内信:网站内部的一种通信方式。
1.用户和用户之间的通信。(点对点)
2.管理员/系统 和 某个用户之间的通信。(点对点) ===》竞赛结果的通信消息
3. 管理员/系统 和 某个用户群(指的是满足某一条件的用户的群体)之间的通信。(点对面)
而且每个用户的消息都不一样
这个是给用户群发消息,福利信息每个用户的消息都一样—》点对面
因为每个用户消息不一样—》点对点
消息的话我们还要设计数据库
因为如果是消息群的话,那么就会把相同的消息发给多个人,所以我们可以设计两个表,一个消息内容表,一个是消息和用户的对应表,这样就不会相同消息发给多个人了
消息内容表
create table tb_message_text(
text_id bigint unsigned NOT NULL COMMENT '消息内容id(主键)',
message_title varchar(10) NOT NULL COMMENT '消息标题',
message_content varchar(200) NOT NULL COMMENT '消息内容',
create_by bigint unsigned not null comment '创建人',
create_time datetime not null comment '创建时间',
update_by bigint unsigned comment '更新人',
update_time datetime comment '更新时间',
primary key (text_id)
)
# 消息表
create table tb_message(
message_id bigint unsigned NOT NULL COMMENT '消息id(主键)',
text_id bigint unsigned NOT NULL COMMENT '消息内容id(主键)',
send_id bigint unsigned NOT NULL COMMENT '消息发送人id',
rec_id bigint unsigned NOT NULL COMMENT '消息接收人id',
create_by bigint unsigned not null comment '创建人',
create_time datetime not null comment '创建时间',
update_by bigint unsigned comment '更新人',
update_time datetime comment '更新时间',
primary key (message_id)
);
消息如何产生—》竞赛结果通知消息–》凌晨统计排名,对当天结束的竞赛进行排名的统计—》产生消息—》竞赛结束时间不能超过晚上十点—>把消息存在数据库中,然后查询就可以了
然后消息也要存在redis中,不然还是太慢了
一个是user:message:list:userId,存储list,每个元素是消息id
还有一个是message:detail:textId,存储的是JSON,消息详情
1.2 消息发送
通过定时任务生成消息–.>存储到数据库和缓存中,获取消息列表的时候就可以从缓存中获取了,注意生成消息的时候只存在缓存中
缓存没有修改和删除
@Data
public class UserScore {
private Long examId;
private Long userId;
private Integer score;
}
这个是新增加的类
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ck.job.mapper.user.UserSubmitMapper">
<select id="selectUserScoreList" resultType="com.ck.job.domain.user.UserScore">
SELECT
user_id,
exam_id,
sum(score) as score
FROM
tb_user_submit
<where>
<foreach collection="examIdSet" open="exam_id in (" close=")" item="examId" separator="," >
#{examId}
</foreach>
</where>
GROUP BY
user_id , exam_id
ORDER BY
score DESC
</select>
</mapper>
这个是使用的xml
@Service
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements IMessageService{
@Override
public boolean batchInsert(List<Message> messageList){
return saveBatch(messageList);
}
}
@Service
public class MessageTextServiceImpl extends ServiceImpl<MessageTextMapper, MessageText> implements IMessageTextService {
@Override
public boolean batchInsert(List<MessageText> messageTextList){
return saveBatch(messageTextList);
}
}
这个是批量插入的service方法
public static final Long SYSTEM_USER_ID = 1L;
因为createBy无法获取用户Id,所以我们提前要设置好
public static final String USER_MESSAGE_LIST_USERID = "user:message:list:";
public static final String MESSAGE_DETAIL_MESSAGEID = "message:detail:";
这个是存入缓存的结构
@Data
public class MessageCacheVO {
private String messageTitle;
private String messageContent;
}
这个是存入信息详细数据的类
最后展示定时器的代码
@XxlJob("examResultHandler")
public void examResultHandler(){
log.info("*****examResultHandler:凌晨统计排名*****");
//先从数据库中获取所有已经结束的竞赛列表
LocalDateTime now = LocalDateTime.now();
LocalDateTime minusDays = now.minusDays(1);//获取小于一天的时间
List<Exam> examList = examMapper.selectList(new LambdaQueryWrapper<Exam>()
.select(Exam::getExamId,Exam::getTitle)
.ge(Exam::getEndTime, minusDays)
.le(Exam::getEndTime, now)//小于等于当前时间
.eq(Exam::getStatus, Constants.TRUE));//已经发布)
if(CollectionUtil.isEmpty(examList)){
return;
}
Set<Long> examIdSet = examList.stream().map(Exam::getExamId).collect(Collectors.toSet());
//然后根据examIdSet获取所有用户的分数
List<UserScore> userScoreList = userSubmitMapper.selectUserScoreList(examIdSet);
Map<Long, List<UserScore>> examIdUserScoreMap = userScoreList.stream().collect(Collectors.groupingBy(UserScore::getExamId));
//按照examId分组,那么分数排名就已经OK了
saveMessage(examList,examIdUserScoreMap);
}
private void saveMessage(List<Exam> examList, Map<Long, List<UserScore>> examIdUserScoreMap ) {
List<Message> messageList = new ArrayList<>();//插入msg与用户对应信息数据库
List<MessageText> messageTextList = new ArrayList<>();//插入数据库,msg详细信息
for(Exam exam: examList){
Long examId = exam.getExamId();
List<UserScore> userScoreList = examIdUserScoreMap.get(examId);
int userTotal = userScoreList.size();
int rank = 1;
for (UserScore userScore : userScoreList){
String msgTitle = exam.getTitle()+"——排名情况";
String msgContent = "你参加的竞赛:"+ exam.getTitle()+",你的分数为:"
+userScore.getScore()+",总人数:+"+userTotal +",你的排名为:"+rank;
rank++;
MessageText messageText = new MessageText();
messageText.setMessageTitle(msgTitle);
messageText.setMessageContent(msgContent);
messageText.setCreateBy(Constants.SYSTEM_USER_ID);
messageTextList.add(messageText);
Message message = new Message();
message.setSendId(Constants.SYSTEM_USER_ID);
message.setRecId(userScore.getUserId());
message.setCreateBy(Constants.SYSTEM_USER_ID);
messageList.add(message);
}
}
messageTextService.batchInsert(messageTextList);
//给messageList添加messageId
Map<String, MessageCacheVO> messageCacheVOMap = new HashMap<>();//存入redis,信息详细数据
for(int i=0;i<messageTextList.size();i++){
MessageText messageText = messageTextList.get(i);
Message message = messageList.get(i);
message.setTextId(messageText.getTextId());
MessageCacheVO messageCacheVO = new MessageCacheVO();
messageCacheVO.setMessageContent(messageText.getMessageContent());
messageCacheVO.setMessageTitle(messageText.getMessageTitle());
String messageDetailKey = getMessageDetailKey(messageText.getTextId());
messageCacheVOMap.put(messageDetailKey,messageCacheVO);
}
redisService.multiSet(messageCacheVOMap);
messageService.batchInsert(messageList);
//存入缓存,用户的信息列表---》那么就要把信息按照userId进行分组了
Map<Long, List<Message>> userMsgMap = messageList.stream().collect(Collectors.groupingBy(Message::getRecId));
Iterator<Map.Entry<Long, List<Message>>> iterator = userMsgMap.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<Long, List<Message>> entry = iterator.next();
Long userId = entry.getKey();
String userMessageListKey = getUserMessageListKey(userId);
List<Message> userMessageList = entry.getValue();
List<Long> userMsgIdList = userMessageList.stream().map(Message::getTextId).toList();
redisService.rightPushAll(userMessageListKey,userMsgIdList);
}
// for (Map.Entry<Long, List<Message>> entry : userMsgMap.entrySet()) {
// Long userId = entry.getKey();
// String userMessageListKey = getUserMessageListKey(userId);
// List<Message> userMessageList = entry.getValue();
// List<Long> userMsgIdList = userMessageList.stream().map(Message::getTextId).toList();
// redisService.rightPushAll(userMessageListKey, userMsgIdList);
// }
}
private String getUserMessageListKey(Long userId) {
return CacheConstants.USER_MESSAGE_LIST_USERID + userId;
}
private String getMessageDetailKey(Long messageTextId) {
return CacheConstants.MESSAGE_DETAIL_MESSAGEID + messageTextId;
}
我们使用的都是批量插入
在 Java 中,Iterator(迭代器)初始化后,其初始状态是指向集合中第一个元素的「前面」
1.3 消息列表展示
创建一个新的controller,UserMessageController
@RestController
@RequestMapping("/user/message")
@Tag(name = "C端用户信息接口")
@Slf4j
public class UserMessageController {
@Autowired
private IUserMessageService userMessageService;
@GetMapping("/list")
@Operation(description = "获取用户接收到的信息")
public TableDataInfo list(PageQueryDTO dto){
log.info("获取用户接收到的信息,PageQueryDTO:{}", dto);
return userMessageService.list(dto);
}
}
直接拷贝以前获取竞赛列表的代码,然后改吧改吧
@Data
public class MessageCacheVO {
private Long textId;
private String messageTitle;
private String messageContent;
}
这个类要完善一下,因为从数据库中可以查询出这个类,然后刷新缓存就要用到messageTextId,记得定时器存入缓存的时候,这个字段也要完善
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ck.friend.mapper.message.MessageTextMapper">
<select id="selectUserMsgList" resultType="com.ck.friend.domain.message.vo.MessageCacheVO">
SELECT
text_id,
message_title,
message_content,
FROM
tb_message m
JOIN
tb_message_text tm
ON
m.text_id = tm.text_id
<where>
m.rec_id = #{userId}
</where>
ORDER BY
t.create_time DESC
</select>
</mapper>
这个是根据userId查询它的所有的信息的xml
@Override
public TableDataInfo list(PageQueryDTO dto) {
Long userId= ThreadLocalUtil.get(Constants.USER_ID,Long.class);
Long listSize = userMessageCacheManager.getListSize(userId);
List<MessageCacheVO> list ;
if(listSize==null||listSize==0){
//说明缓存中没有数据,所以要先从数据库中获取数据,然后存入redis
PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
list = messageTextMapper.selectUserMsgList(userId);
userMessageCacheManager.refreshCache(userId);
long total = new PageInfo<>(list).getTotal();
return TableDataInfo.success(list, total);
}else{
//直接从redis中获取数据
list = userMessageCacheManager.getUserMsgList(dto,userId);
listSize = userMessageCacheManager.getListSize(userId);
return TableDataInfo.success(list, listSize);
}
}
这是service代码
然后是userMessageCacheManager的代码
@Component
public class UserMessageCacheManager {
@Autowired
private RedisService redisService;
@Autowired
private MessageTextMapper messageTextMapper;
public Long getListSize(Long userId) {
String userMessageListKey = getUserMessageListKey(userId);
return redisService.getListSize(userMessageListKey);
}
public void refreshCache(Long userId) {
List<MessageCacheVO> messageCacheVOList = new ArrayList<>();
messageCacheVOList = messageTextMapper.selectUserMsgList(userId);//没有分页
List<Long> userMsgIdList = messageCacheVOList.stream().map(MessageCacheVO::getTextId).toList();
if (CollectionUtil.isEmpty(messageCacheVOList)) {
return;
}
redisService.rightPushAll(getUserMessageListKey(userId), userMsgIdList); //刷新列表缓存
//刷新信息详情缓存
Map<String, MessageCacheVO> messageCacheVOMap = new HashMap<>();
for (MessageCacheVO messageCacheVO : messageCacheVOList) {
messageCacheVOMap.put(getMessageDetailKey(messageCacheVO.getTextId()),messageCacheVO);
}
redisService.multiSet(messageCacheVOMap); //刷新详情缓存
}
public List<MessageCacheVO> getUserMsgList(PageQueryDTO dto, Long userId) {
int start = (dto.getPageNum() - 1) * dto.getPageSize();
int end = start + dto.getPageSize() - 1; //下标需要 -1
String userMessageListKey = getUserMessageListKey(userId);
List<Long> messageIdList = redisService.getCacheListByRange(userMessageListKey, start, end, Long.class);
List<MessageCacheVO> messageCacheVOList = assembleExamVOList(messageIdList);//从缓存中加载详情
if (CollectionUtil.isEmpty(messageCacheVOList)) {
//说明redis中数据可能有问题 从数据库中查数据并且重新刷新缓存
messageCacheVOList = getMessageVOListByDB(dto,userId); //从数据库中获取数据
refreshCache(userId);
}
return messageCacheVOList;
}
private List<MessageCacheVO> getMessageVOListByDB(PageQueryDTO dto, Long userId) {
PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
return messageTextMapper.selectUserMsgList(userId);
}
private List<MessageCacheVO> assembleExamVOList(List<Long> messageIdList) {
if (CollectionUtil.isEmpty(messageIdList)) {
//说明redis当中没数据 从数据库中查数据并且重新刷新缓存
return null;
}
//拼接redis当中key的方法 并且将拼接好的key存储到一个list中
List<String> detailKeyList = new ArrayList<>();
for (Long messageTextId : messageIdList) {
detailKeyList.add(getMessageDetailKey(messageTextId));
}
List<MessageCacheVO> messageCacheVOList = redisService.multiGet(detailKeyList, MessageCacheVO.class);
CollUtil.removeNull(messageCacheVOList);
if (CollectionUtil.isEmpty(messageCacheVOList) || messageCacheVOList.size() != messageIdList.size()) {
//说明redis中数据有问题 从数据库中查数据并且重新刷新缓存
return null;
}
return messageCacheVOList;
}
private String getUserMessageListKey(Long userId) {
return CacheConstants.USER_MESSAGE_LIST_USERID + userId;
}
private String getMessageDetailKey(Long messageTextId) {
return CacheConstants.MESSAGE_DETAIL_MESSAGEID + messageTextId;
}
}
1.4 前端开发
创建文件UserMessage.vue
<template>
<div class="message-list">
<div class="message-list-block">
<div class="message-list-header">
<span class="ms-title">我的消息</span>
<span class="message-list-back" @click="goBack()">返回</span>
</div>
<div class="mesage-list-content" v-for="(item, index) in messageList" :key="index">
<img src="@/assets/message/notice.png" width="50px" class="image" />
<div class="message-content">
<div class="title-box">
<div class="title">
{{ item.messageTitle }}
</div>
</div>
<div class="content">{{ item.messageContent }}</div>
</div>
<el-button class="mesage-button" type="text" @click.stop="handlerDelete(item)">删除</el-button>
</div>
<div class="message-pagination">
<!-- 增加分页展示器 -->
<el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
v-model:current-page="params.pageNum" v-model:page-size="params.pageSize"
:page-sizes="[5, 10, 15, 20]" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
</div>
</div>
</template>
<script setup>
import { getMessageListService } from "@/apis/message"
import router from "@/router"
import { reactive, ref } from "vue"
const messageList = ref([]) //消息列表
const total = ref(0)
const params = reactive({
pageNum: 1,
pageSize: 10,
})
//消息列表
async function getMessageList() {
const ref = await getMessageListService(params)
messageList.value = ref.rows
total.value = ref.total
}
getMessageList()
const goBack = () => {
router.go(-1)
}
// 分页
function handleSizeChange(newSize) {
params.pageNum = 1
getMessageList()
}
function handleCurrentChange(newPage) {
getMessageList()
}
</script>
import service from "@/utils/request";
export function getMessageListService(params) {
return service({
url: "/user/message/list",
method: "get",
params,
});
}
然后就可以测试了
我们创建三个用户来测试一下
<select id="selectUserMsgList" resultType="com.ck.friend.domain.message.vo.MessageCacheVO">
SELECT
tm.text_id,
tm.message_title,
tm.message_content
FROM
tb_message m
JOIN
tb_message_text tm
ON
m.text_id = tm.text_id
WHERE
m.rec_id = #{userId}
ORDER BY
m.create_time DESC
</select>
然后发现xml文件有问题
修改一下
成功了
2. 竞赛排名功能
就是历史竞赛那里的排名功能
未完赛没有查看排名功能
排名和得分和用户id都有了
但是得分和排名还没有存储—》tb_user_exam有排名的得分字段
----》不用重新统计了–》直接获取—》存入数据库,在定时器的时候
然后还要存入缓存–》key为exam:rank:list:examId
value为userId?不是,第一我们是为了防止一个数据存储多份,所以才存id,但是这里的排名是不会存储多份的,因为不同竞赛的排名和分数是不一样的,而且排名和分数是不能修改的
所以value就是需要什么存什么–》json–>examRank,nickName,score,其中examRank和score在不同竞赛中一般是不同的
但是nickName是会重复的,而且用户修改nickname还要改redis—》所以可以存userId,然后由userId获取redis中的nickname
我们现在定时器那里,修改tb_user_exam表,完善score和exam_rank字段
然后往redis中存入排名数据
@Data
public class UserScore {
private Long examId;
private Long userId;
private Integer score;
private Integer examRank;
}
完善一下这个类,这个是可以直接存入数据库中,什么都有了
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ck.job.mapper.user.UserExamMapper">
<update id="updateScoreAndExamRank">
<foreach collection="userScoreList" item="item" separator=";">
UPDATE
tb_user_exam
SET
score = #{item.score}, exam_rank = #{item.examRank}
WHERE
exam_id = #{item.examId} AND user_id = #{item.userId}
</foreach>
</update>
</mapper>
这个是修改数据库tb_user_exam的xml语句
public static final String EXAM_RANK_LIST_EXAMID = "exam:rank:list:";
private String getExamRankListKey(Long examId) {
return CacheConstants.EXAM_RANK_LIST_EXAMID + examId;
}
这个是redis的key
在savemessage方法中
for(Exam exam: examList){
Long examId = exam.getExamId();
List<UserScore> userScoreList = examIdUserScoreMap.get(examId);
int userTotal = userScoreList.size();
int rank = 1;
for (UserScore userScore : userScoreList){
String msgTitle = exam.getTitle()+"——排名情况";
String msgContent = "你参加的竞赛:"+ exam.getTitle()+",你的分数为:"
+userScore.getScore()+",总人数:"+userTotal +",你的排名为:"+rank;
userScore.setExamRank(rank);
rank++;
MessageText messageText = new MessageText();
messageText.setMessageTitle(msgTitle);
messageText.setMessageContent(msgContent);
messageText.setCreateBy(Constants.SYSTEM_USER_ID);
messageTextList.add(messageText);
Message message = new Message();
message.setSendId(Constants.SYSTEM_USER_ID);
message.setRecId(userScore.getUserId());
message.setCreateBy(Constants.SYSTEM_USER_ID);
messageList.add(message);
}
userExamMapper.updateScoreAndExamRank(userScoreList);
redisService.rightPushAll(getExamRankListKey(examId),userScoreList);
}
然后是创建查询的controller
@GetMapping("/rank/list")
public TableDataInfo rankList(RankQueryDTO rankQueryDTO){
log.info("获取竞赛排名列表信息,rankQueryDTO:{}", rankQueryDTO);
return examService.rankList(rankQueryDTO);
}
@Data
public class RankQueryDTO extends PageQueryDTO {
private Long examId;
}
然后service
@Data
public class ExamRankCacheVO {
private String nickName;
private Long userId;
private Integer score;
private Integer examRank;
}
这个类是从缓存中要获取的数据,其中只用获取后面三个字段,第一个字段是再次获取的,再次从redis中获取
<select id="selectExamRankCacheVOList" resultType="com.ck.friend.domain.exam.vo.ExamRankCacheVO">
SELECT
user_id,
score,
exam_rank
FROM
tb_user_exam
WHERE
exam_id = #{examId}
ORDER BY
exam_rank
</select>
这个是从数据库中获取排名信息的xml语句,根据examId
@Override
public TableDataInfo rankList(RankQueryDTO rankQueryDTO) {
Long listSize = examCacheManager.getExamRankListSize(rankQueryDTO.getExamId());
List<ExamRankCacheVO> list;
if(listSize==null||listSize==0){
//说明缓存中没有数据,所以要先从数据库中获取数据,然后存入redis
PageHelper.startPage(rankQueryDTO.getPageNum(), rankQueryDTO.getPageSize());
list = examMapper.selectExamRankCacheVOList(rankQueryDTO.getExamId());
examCacheManager.refreshExamRankListCache(rankQueryDTO.getExamId());
listSize = new PageInfo<>(list).getTotal();
}else{
//直接从redis中获取数据
list = examCacheManager.getExamRankList(rankQueryDTO);
}
assembleExamRankList(list);
return TableDataInfo.success(list, listSize);
}
private void assembleExamRankList(List<ExamRankCacheVO> list) {
if(CollectionUtil.isEmpty(list)){
return;
}
for (ExamRankCacheVO examRankCacheVO : list) {
UserVO user = userCacheManager.getUserById(examRankCacheVO.getUserId());
examRankCacheVO.setNickName(user.getNickName());
}
}
然后是examCacheManager中的方法
//竞赛排名
public Long getExamRankListSize(Long examId) {
return redisService.getListSize(getExamRankListKey(examId));
}
private String getExamRankListKey(Long examId) {
return CacheConstants.EXAM_RANK_LIST_EXAMID + examId;
}
public void refreshExamRankListCache(Long examId) {
//没有分页查询
List<ExamRankCacheVO> examRankCacheVOList = examMapper.selectExamRankCacheVOList(examId);
redisService.rightPushAll(getExamRankListKey(examId),examRankCacheVOList);
}
public List<ExamRankCacheVO> getExamRankList(RankQueryDTO rankQueryDTO) {
int start = (rankQueryDTO.getPageNum() - 1) * rankQueryDTO.getPageSize();
int end = start + rankQueryDTO.getPageSize() - 1; //下标需要 -1
return redisService.getCacheListByRange(getExamRankListKey(rankQueryDTO.getExamId()),start,end,ExamRankCacheVO.class);
}
这样就OK了
然后拷贝前端代码到exam.vue
<el-dialog v-model="dialogVisible" width="600px" top="30vh" :show-close="true" :close-on-click-modal="false"
:close-on-press-escape="false" class="oj-login-dialog-centor" center>
<el-table :data="examRankList">
<el-table-column label="排名" prop="examRank" />
<el-table-column label="用户昵称" prop="nickName" />
<el-table-column label="用户得分" prop="score" />
</el-table>
<el-pagination class="range_page" background layout="total, sizes, prev, pager, next, jumper" :total="rankTotal"
v-model:current-page="rankParams.pageNum" v-model:page-size="rankParams.pageSize" :page-sizes="[5, 10, 15, 20]"
@size-change="handleRankSizeChange" @current-change="handleRankCurrentChange" />
</el-dialog>
//竞赛排名
const rankParams = reactive({
examId:'',
pageNum: 1,
pageSize: 9,
})
const examRankList = ref([])
const rankTotal = ref(0)
// 分页
function handleRankSizeChange(newSize) {
rankParams.pageNum = 1
getExamRankList()
}
function handleRankCurrentChange(newPage) {
getExamRankList()
}
const dialogVisible = ref(false)
async function getExamRankList() {
const result = await getExamRankListService(rankParams)
examRankList.value = result.rows
rankTotal.value = result.total
}
function togglePopover(examId) {
dialogVisible.value = true
rankParams.examId = examId
getExamRankList()
}
export function getExamRankListService(params) {
return service({
url: "/exam/rank/list",
method: "get",
params,
});
}
然后就可以进行测试了
但是有一个要注意的点就是
我们的sql是不支持批量update的
要加上allowMultiQueries=true才可以
这样就成功了