微服务的编程测评系统19-我的消息功能-竞赛排名功能
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. 我的消息功能
- 1.1 业务分析
- 1.2 消息发送
- 1.3 消息列表展示
- 1.4 前端开发
- 2. 竞赛排名功能
- 总结8
前言
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 mapperPUBLIC "-//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">SELECTuser_id,exam_id,sum(score) as scoreFROMtb_user_submit<where><foreach collection="examIdSet" open="exam_id in (" close=")" item="examId" separator="," > #{examId}</foreach></where>GROUP BY user_id , exam_idORDER BYscore DESC</select>
</mapper>
这个是使用的xml
@Service
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements IMessageService{@Overridepublic boolean batchInsert(List<Message> messageList){return saveBatch(messageList);}
}
@Service
public class MessageTextServiceImpl extends ServiceImpl<MessageTextMapper, MessageText> implements IMessageTextService {@Overridepublic 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添加messageIdMap<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 {@Autowiredprivate 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 mapperPUBLIC "-//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">SELECTtext_id,message_title,message_content,FROMtb_message mJOINtb_message_text tmONm.text_id = tm.text_id<where>m.rec_id = #{userId}</where>ORDER BYt.create_time DESC</select>
</mapper>
这个是根据userId查询它的所有的信息的xml
@Overridepublic 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){//说明缓存中没有数据,所以要先从数据库中获取数据,然后存入redisPageHelper.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 {@Autowiredprivate RedisService redisService;@Autowiredprivate 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; //下标需要 -1String 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.rowstotal.value = ref.total
}
getMessageList()const goBack = () => {router.go(-1)
}
// 分页
function handleSizeChange(newSize) {params.pageNum = 1getMessageList()
}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">SELECTtm.text_id,tm.message_title,tm.message_contentFROMtb_message mJOINtb_message_text tmONm.text_id = tm.text_idWHEREm.rec_id = #{userId}ORDER BYm.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 mapperPUBLIC "-//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=";">UPDATEtb_user_examSETscore = #{item.score}, exam_rank = #{item.examRank}WHEREexam_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">SELECTuser_id,score,exam_rankFROMtb_user_examWHEREexam_id = #{examId}ORDER BYexam_rank </select>
这个是从数据库中获取排名信息的xml语句,根据examId
@Overridepublic TableDataInfo rankList(RankQueryDTO rankQueryDTO) {Long listSize = examCacheManager.getExamRankListSize(rankQueryDTO.getExamId());List<ExamRankCacheVO> list;if(listSize==null||listSize==0){//说明缓存中没有数据,所以要先从数据库中获取数据,然后存入redisPageHelper.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; //下标需要 -1return 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 = 1getExamRankList()
}function handleRankCurrentChange(newPage) {getExamRankList()
}const dialogVisible = ref(false)async function getExamRankList() {const result = await getExamRankListService(rankParams)examRankList.value = result.rowsrankTotal.value = result.total
}function togglePopover(examId) {dialogVisible.value = truerankParams.examId = examIdgetExamRankList()
}
export function getExamRankListService(params) {return service({url: "/exam/rank/list",method: "get",params,});
}
然后就可以进行测试了
但是有一个要注意的点就是
我们的sql是不支持批量update的
要加上allowMultiQueries=true才可以
这样就成功了