You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何连接JS与Spring统计用户点击 实现Mongo随机文档不重复返回

实现方案

整套逻辑不需要复杂的前后端联动,核心用服务端会话跟踪单个用户的浏览状态即可,不需要额外引入中间件,具体实现分两部分:

后端改造(Spring + MongoDB部分)

  • 第一步先固定基准值:在Service层新增计数方法,查询adventureholidays集合中type = summerCamps的文档总数,作为点击次数的上限值。
  • 第二步做用户状态隔离:直接用Spring MVC自带的HttpSession存储单个用户的两个状态值,不需要做登录系统也能区分不同访问用户:
    • viewedSummerCampIds:Set结构,存已经返回给当前用户的文档ID,用来做去重
    • summerCampClickCount:整数,存用户点击按钮的次数
      用户第一次访问接口时两个值自动初始化为空集合、0。
  • 第三步改造原有接口逻辑:
    1. 每次接口被调用时,先从Session取出上述两个状态值,再查询符合条件的文档总数
    2. 如果点击次数已经大于等于总文档数,直接返回带“集合内无更多文档”提示的视图
    3. 如果还有剩余文档,查询条件新增_id 不在已浏览ID集合中的规则,从剩余未返回的文档里随机取1条
    4. 取到文档后,把文档ID加入已浏览集合,点击次数+1,更新回Session
    5. 可以额外往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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 15:06:20