如何连接JS与Spring统计用户点击 实现Mongo随机文档不重复返回
实现方案
整套逻辑不需要复杂的前后端联动,核心用服务端会话跟踪单个用户的浏览状态即可,不需要额外引入中间件,具体实现分两部分:
后端改造(Spring + MongoDB部分)
- 第一步先固定基准值:在Service层新增计数方法,查询
adventureholidays集合中type = summerCamps的文档总数,作为点击次数的上限值。 - 第二步做用户状态隔离:直接用Spring MVC自带的
HttpSession存储单个用户的两个状态值,不需要做登录系统也能区分不同访问用户:viewedSummerCampIds:Set结构,存已经返回给当前用户的文档ID,用来做去重summerCampClickCount:整数,存用户点击按钮的次数
用户第一次访问接口时两个值自动初始化为空集合、0。
- 第三步改造原有接口逻辑:
- 每次接口被调用时,先从Session取出上述两个状态值,再查询符合条件的文档总数
- 如果点击次数已经大于等于总文档数,直接返回带“集合内无更多文档”提示的视图
- 如果还有剩余文档,查询条件新增
_id 不在已浏览ID集合中的规则,从剩余未返回的文档里随机取1条 - 取到文档后,把文档ID加入已浏览集合,点击次数+1,更新回Session
- 可以额外往Model里传一个
hasMore布尔值,告诉前端是否还有剩余文档,方便前端提前置灰按钮优化体验
核心控制器代码参考:
@GetMapping("/getRandomSummerCamps") public String getRandomSummerCamps(HttpSession session, Model model) { // 初始化用户会话状态 Set<String> viewedIds = (Set<String>) session.getAttribute("viewedSummerCampIds"); Integer clickCount = (Integer) session.getAttribute("summerCampClickCount"); if (viewedIds == null) { viewedIds = new HashSet<>(); clickCount = 0; } long totalCamps = adventureHolidaysService.countSummerCamps(); // 无剩余文档直接返回提示页 if (clickCount >= totalCamps) { model.addAttribute("message", "集合内无更多文档"); return "noMoreTip"; } // 查未浏览的随机文档 AdventureHoliday camp = adventureHolidaysService.getRandomUnviewedCamp(viewedIds); // 更新会话状态 viewedIds.add(camp.getId()); clickCount++; session.setAttribute("viewedSummerCampIds", viewedIds); session.setAttribute("summerCampClickCount", clickCount); model.addAttribute("randomSummerCamps", camp); model.addAttribute("hasMore", clickCount < totalCamps); return "randomSummerCamps"; }
Service层的MongoDB查询不要用skip随机或者$where这类低性能写法,直接用MongoDB原生的$sample聚合做随机抽取,效率更高,参考实现:
// 统计符合条件的夏令营总数 public long countSummerCamps() { Criteria condition = Criteria.where("type").is("summerCamps"); return mongoTemplate.count(Query.query(condition), AdventureHoliday.class); } // 从未浏览的集合中随机取1条 public AdventureHoliday getRandomUnviewedCamp(Set<String> viewedIds) { Aggregation aggregation = Aggregation.newAggregation( Aggregation.match( Criteria.where("type").is("summerCamps") .and("_id").nin(viewedIds) ), Aggregation.sample(1) ); return mongoTemplate.aggregate(aggregation, "adventureholidays", AdventureHoliday.class) .getUniqueMappedResult(); }
记得给type字段加普通索引,数据量上来之后查询速度会快很多。
前端联动部分
不需要复杂的交互逻辑,两种实现方式选就行:
- 如果保留原有同步刷新的逻辑:按钮直接用a标签或者form表单指向
/getRandomSummerCamps接口即可,点击后后端返回什么页面浏览器就渲染什么,到上限时自动展示提示页,不需要写额外JS。 - 如果要做无刷新的交互体验:给
Randomize again按钮绑定点击事件,用fetch发请求拉取内容,局部替换页面的内容区域即可,拿到后端返回的hasMore标识为false时,直接把按钮置灰禁止点击,减少无效请求。
原生JS实现参考:
<div id="contentArea"></div> <button id="randomBtn">Randomize again</button> <div id="tipArea"></div> <script> const contentArea = document.getElementById('contentArea'); const randomBtn = document.getElementById('randomBtn'); const tipArea = document.getElementById('tipArea'); randomBtn.addEventListener('click', async () => { const resp = await fetch('/getRandomSummerCamps'); // 命中无更多文档的逻辑 if (resp.url.includes('noMoreTip')) { tipArea.textContent = '集合内无更多文档'; randomBtn.disabled = true; return; } // 正常渲染返回的内容 contentArea.innerHTML = await resp.text(); }) </script>
可选优化点
- 如果需要长期记录用户的浏览记录(比如关闭浏览器、隔几天访问也不重复推),就把原来存在Session里的已浏览ID、点击次数迁移到数据库,和用户登录账号绑定即可,核心逻辑不需要改。
- 默认Session有效期是30分钟,超时后用户的浏览状态会自动清空,重新从第一条开始随机,符合一般活动类场景的预期,如果需要调整有效期可以在Spring配置里修改Session超时时间。
内容的提问来源于stack exchange,提问作者Sefan Jankovic
相关产品推荐
相关产品推荐

