【SpringBoot+自定义注解+Aop+长轮询】实现简单的实时消息推送


前言

小白摸索,大佬勿喷

我们平常在网站上都可以看到右上角的消息上未读消息数量,以及有消息时右下角实时弹出的例子

我公司项目刚好也有这个需求,但是由于甲方要求不能使用第三方服务,于是便有了本文的解决思路


一、应用场景

该思路适用于一些无法使用第三方服务的场景,如果可以使用第三方服务仍旧是使用第三方的服务比较好

二、使用步骤

1.引入依赖

这里我用到的依赖有:aspectj、mybatis-plus、jjwt、hutool等,大家可以自行选择

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
            <version>2.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.6</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.20</version>
        </dependency>

2.数据库建立

消息表:

CREATE TABLE `t_message` (
  `id` int NOT NULL AUTO_INCREMENT,
  `content` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '内容',
  `from_uid` int DEFAULT NULL COMMENT '发起人id',
  `to_uid` int DEFAULT NULL COMMENT '接收人id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

部门表:

CREATE TABLE `t_dept` (
  `id` int NOT NULL AUTO_INCREMENT,
  `dept_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '部门名称',
  `dept_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '部门编码',
  `leader_uid` int DEFAULT NULL COMMENT '部门领导id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

用户表:

CREATE TABLE `t_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码',
  `real_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '真实姓名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

3.编写业务代码

为了方便演示我这里使用的是MybatisPlus生成后自带的方法去添加,@RealTimeMessage为自定义注解,下面会提到

/**
     * 添加部门
     *
     * @param dept 部门实体类
     * @return String
     */
    @RealTimeMessage("添加部门")
    @PostMapping("/add")
    public String addDept(@RequestBody Dept dept) {
        boolean save = deptService.save(dept);
        if (save) {
            return "SUCCESS";
        } else {
            return "FAILED";
        }
    }

4.Token相关

JwtUtils:

@Component
public class JwtUtils {

    private final String KEY = "abcdefghijklmnopqrstuvwxyz";

    /**
     * 创建token
     *
     * @param user 用户实体
     * @return String
     */
    public String createToken(User user) {
        Map<String, Object> claims = BeanUtil.beanToMap(user);
        JwtBuilder jwtBuilder = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS512, KEY);
        return jwtBuilder.compact();
    }

    /**
     * 解析token
     *
     * @param token token
     * @return User
     */
    public User parseToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(KEY)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (JwtException e) {
            claims = null;
            e.printStackTrace();
        }
        return BeanUtil.toBean(claims, User.class);
    }

}

TokenService及实现类:

// TokenService
public interface TokenService {
	/**
	* 获取当前登录用户的信息
	* @return User 
	*/
    User getUserByToken();
}


// TokenService的实现类
@Service
public class TokenServiceImpl implements TokenService {

    @Resource
    private JwtUtils jwtUtils;

    @Override
    public User getUserByToken() {
        HttpServletRequest request = getRequest();
        String token = request.getHeader("token");
        return jwtUtils.parseToken(token);
    }

    public HttpServletRequest getRequest(){
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

5.编写队列相关方法

@Component
public class MessageQueue {

    /**
     * 存放消息的map集合
     */
    private static final Map<User, Queue<Message>> MSG_MAP = new ConcurrentHashMap<>();
    private static final Map<User, Integer> RETURN_TIME = new ConcurrentHashMap<>();

    /**
     * 注册当前请求用户到map中
     *
     * @param user 当前用户
     */
    public void register(User user) {
        MSG_MAP.put(user, new ConcurrentLinkedDeque<>());
        RETURN_TIME.put(user, 0);
    }

    /**
     * 销毁已注册的用户
     *
     * @param user 当前用户
     */
    public void remove(User user) {
        MSG_MAP.remove(user);
        RETURN_TIME.remove(user);
    }

    /**
     * 将消息存入对应的接收方队列中
     *
     * @param message 要接收的消息
     */
    public void push(Message message) {
        MSG_MAP.keySet().forEach(a -> {
            synchronized (a) {
                if (a.getId().equals(message.getToUid())) {
                    MSG_MAP.get(a).add(message);
                    a.notifyAll();
                }
            }
        });
    }

    /**
     * 推送消息
     *
     * @param user 当前用户实体
     * @return List<Message>
     */
    public List<Message> pollAll(User user) {
        Map<User,Queue<Message>> map1 = MSG_MAP;
        Map<User,Integer> map2 = RETURN_TIME;
        List<Message> messageList = new ArrayList<>();
        Queue<Message> messageQueue = MSG_MAP.get(user);
        //如果队列不为空,则将消息添加到对应的队列中  否则将该用户注册到map中
        if (messageQueue != null) {
            messageQueue.forEach(a -> {
                Message poll = messageQueue.poll();
                if (poll != null) {
                    messageList.add(poll);
                }
            });
        } else {
            register(user);
        }

        //如果消息为空则将服务阻塞 不响应前端请求 直到map中有新消息则返回消息 
        //此时需要与前端配合设置阻塞时间 可根据实际需求更改
        if (messageList.isEmpty() && RETURN_TIME.get(user) < 3) {
            try {
                Integer time = RETURN_TIME.get(user);
                RETURN_TIME.put(user, time + 1);
                synchronized (user) {
                    user.wait(10000);
                }
                return pollAll(user);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        return messageList;

    }

}

6.自定义注解+AOP

自定义注解,value用来存放操作名称

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RealTimeMessage {

    String value();
}

编写切面去拦截添加了自定义注解的方法

@Aspect
@Component
public class RealTimeMessageAop {

    @Resource
    private MessageService messageService;

    @Resource
    private UserService userService;

    @Resource
    private DeptService deptService;

    @Resource
    private TokenService tokenService;

	//
    @Resource
    private MessageQueue messageQueue;

    @Pointcut("@annotation(com.example.validator.anno.RealTimeMessage)")
    public void realTimeMessage() {
    }

    @Around("realTimeMessage()")
    public Object check(ProceedingJoinPoint point) throws Throwable {
        Object obj;
        //获取当前请求用户对象
        User user = tokenService.getUserByToken();
        //获取自定义注解
        Method method=((MethodSignature)point.getSignature()).getMethod();
        Signature signature = point.getSignature();
        Method realMethod = point.getTarget().getClass().getDeclaredMethod(signature.getName(), method.getParameterTypes());
        RealTimeMessage anno = realMethod.getAnnotation(RealTimeMessage.class);
        //获取前端传参,并转为map集合
        Object[] args = point.getArgs();
        Dept dept = BeanUtil.toBean(args[0], Dept.class);
        //继续执行原有方法
        obj = point.proceed();
        //获取返回消息
        String result = String.valueOf(obj);
        //如果请求成功则弹出消息 否则不做操作
        if ("SUCCESS".equals(result)) {
            //添加消息到数据库中
            Message build = Message.builder()
                    .content(anno.value())
                    .fromUid(user.getId())
                    .toUid(dept.getLeaderUid())
                    .build();
            messageService.save(build);
            //发送实时消息
            messageQueue.push(build);
        }
        return obj;
    }

}

此处我想实现的效果为当管理员添加部门,并指定该部门的领导时,将通知实时发送给管理员设置的领导

7.长轮询

该接口登陆后由前端调用,到达约定时间或返回结果后,前端再次调用该接口实现长轮询

@RestController
@RequestMapping("/message")
public class MessageController {

    @Resource
    private MessageQueue messageQueue;

    @Resource
    private TokenService tokenService;

	/**
	* 拉取实时消息
	* @return List<Message> 
	*/
    @GetMapping("/realTimeMsg")
    public List<Message> getRealTimeMsg() {
        //获取当前请求用户信息
        User user = tokenService.getUserByToken();
        try {
            //注册该用户
            messageQueue.register(user);
            //推送消息
            return messageQueue.pollAll(user);
        } catch (Exception e) {
            throw new IllegalStateException();
        } finally {
            //销毁注册
            messageQueue.remove(user);
        }
    }

功能测试

自此我们的代码已经全部编写完成,接下来就启动项目看一下效果

然后打开我们的接口调试工具把接口与数据填进去
添加部门接口调试

拉取实时消息接口调试

我们还需要Token的参与,为了方便演示我们就直接在生成所需要的Token

Token生成单元测试
接下来我们把管理员Token填入添加单位的接口的Header
添加管理员Token到添加部门接口

然后我们把任命领导的Token填入拉取消息的Header

添加领导Token到拉取消息接口

我们首先调用拉取实时消息的接口,接口在没有消息的时候会一直阻塞,直到到达约定时间,如果到了约定时间仍无新消息则直接返回空列表,效果如下:
请求阻塞调用拉取实时消息接口

接着我们重新调用拉取实时消息的接口,并发送添加部门的请求,这时我们可以看到消息已经推送过来了,效果如下:
调用添加部门接口
拉取实时消息接口

自此我们的全部功能就已经完成啦


总结

本文提供了一种简单实时消息推送的一种思路,在使用时可根据实际需求进行修改,该思路主要工作在后端实现,前端只需要轮番调用拉取消息的接口即可


推荐

关注博客和公众号获取最新文章

Bummon’s BlogBummon’s Home公众号

<think>嗯,用户现在想了解如何使用SpringBoot和Vue来实现家长控制孩子访问网站时长的功能。我需要结合之前的对话历史和提供的引用内容来给出一个合理的解决方案。首先,用户之前提到了家长控制使用时长的问题,我的回答涉及到了现有的软件功能以及自定义开发的思路,包括数据存储、时间监控算法和用户界面设计。现在用户具体问到了如何用SpringBoot和Vue来实现,可能需要更详细的技术指导。 首先,我需要分析用户提供的引用内容。引用1、3、4提到了幼儿园管理系统、儿童教育网站和家校互联系统,这些系统都涉及到家长与学校之间的互动,数据管理和权限控制。特别是引用4中的家校互联系统,使用了SpringBoot和Vue,并且有权限管理模块,这可能对当前的问题有帮助。用户的需求是控制孩子访问网站的时长,所以需要设计一个系统,家长可以设置时间限制,系统监控孩子的使用情况,并在超时后限制访问。 接下来,我需要考虑技术实现。后端使用SpringBoot,前端用Vue,这符合用户的要求。系统需要包括用户认证、权限管理、时长设置、访问监控、拦截机制和通知功能。引用4中的家校系统有角色管理和权限控制,可以作为参考,比如家长和孩子不同的角色权限。 数据库设计方面,可能需要用户表、设备/网站表、时间规则表、使用记录表。引用1中的幼儿园管理系统有用户偏好存储,引用4中的家校系统有学生档案和成绩等模块,可以参考这些结构。时间规则表需要记录每天允许的时长和生效时间段,使用记录表记录孩子的访问情况。 后端实现部分,需要RESTful API来处理规则的增删改查,实时监控孩子的访问。使用AOP或者拦截器来检查每次请求的时间是否超限,引用中的系统可能用了类似的方法进行权限控制。定时任务用来计算当天的总使用时间,可能需要用Spring的@Scheduled注解。 前端方面,Vue需要家长界面设置规则,孩子界面显示剩余时间。实时通知可以用WebSocket,但考虑到复杂度,也可以使用轮询或者SSE。拦截部分可能需要在前端路由守卫中检查时间,但后端也需要验证,防止绕过前端限制。 安全方面,引用中的系统提到了权限精确到按钮级别,这里需要确保家长只能管理自己的孩子,孩子不能修改规则。JWT认证和Spring Security可以用于权限控制,参考引用4的做法。 部署方面,引用4的项目结构可能使用了前后端分离,Nginx反向代理,这可以作为部署的参考。 可能的问题包括如何准确记录孩子的访问时间,避免孩子绕过限制,以及实时通知的实现。需要结合后端拦截和前端提示,同时使用数据库记录每次访问的起始和结束时间,计算总时长。当超时时,后端拒绝请求,前端提示并跳转。 现在要组织这些思路,分步骤给出实现方案,确保引用相关的例子,比如家校系统的权限管理,幼儿园管理系统的数据记录等。同时,用户可能希望具体的代码示例,比如SpringBoot的拦截器,Vue的界面组件,以及数据库的SQL语句。 还需要考虑如何整合现有的模块,比如用户管理来自引用4,可以复用其角色和权限模块。时间监控的算法在之前的伪代码中有提到,现在需要具体化为SpringBoot的服务。 最后,生成相关问题,引导用户进一步思考,比如安全措施、扩展功能或实时监控的实现,这些在引用中提到的权限、个性化推荐等可以作为参考。</think>以下是基于SpringBoot+Vue实现家长控制孩子访问网站时长功能的详细技术方案,结合您提供的引用内容中相关系统的设计思路: ### 一、系统架构设计 采用分层架构模式: ``` 前端(Vue.js) │ ├─家长端:网站管理/规则设置/数据看板 ├─孩子端:剩余时长显示/访问受限提示 │ 后端(SpringBoot) │ ├─用户认证模块(JWT+Spring Security)[^4] ├─网站访问拦截模块(Filter/AOP) ├─时间计算引擎(Quartz定时器) ├─数据统计模块(MySQL聚合查询) │ 数据库(MySQL) │ ├─用户表(继承家校系统用户体系[^4]) ├─网站访问规则表 ├─访问日志表 ``` ### 二、核心功能实现 #### 1. 数据库设计(扩展家校系统[^4]) ```sql -- 网站访问规则表(家长设置) CREATE TABLE access_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, child_id BIGINT NOT NULL, -- 关联用户表[^4] max_daily_minutes INT DEFAULT 120, -- 每日最大时长 allowed_periods JSON, -- 允许时间段 [{"start":"09:00","end":"21:00"}] blocked_sites TEXT, -- 禁止访问的域名列表 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (child_id) REFERENCES sys_user(id) ); -- 网站访问日志表(自动记录) CREATE TABLE access_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, child_id BIGINT NOT NULL, domain VARCHAR(255) NOT NULL, -- 访问域名 start_time DATETIME NOT NULL, end_time DATETIME, duration INT -- 单位:秒 ); ``` #### 2. SpringBoot后端实现 ##### 关键代码示例: ```java // 访问拦截AOP(参考家校系统权限控制[^4]) @Aspect @Component public class AccessControlAspect { @Autowired private AccessRuleService ruleService; @Around("@annotation(org.springframework.web.bind.annotation.GetMapping)") public Object checkAccessTime(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); User child = getCurrentUser(); // 从JWT获取当前用户 AccessRule rule = ruleService.getRuleByChildId(child.getId()); int usedTime = accessLogService.getTodayUsedTime(child.getId()); if(usedTime >= rule.getMaxDailyMinutes()*60) { throw new AccessDeniedException("今日使用时长已用尽"); } return joinPoint.proceed(); } } // 定时计算剩余时长(Quartz实现) @Service public class TimeCalculationJob { @Scheduled(cron = "0 0/1 * * * ?") // 每分钟执行 public void updateRemainingTime() { List<ChildUser> children = userService.getAllChildren(); children.forEach(child -> { int used = accessLogService.getTodayUsedTime(child.getId()); int remaining = child.getRule().getMaxDailyMinutes()*60 - used; websocketService.pushRemainingTime(child.getId(), remaining); }); } } ``` #### 3. Vue前端实现 ##### 家长端功能组件: ```vue <template> <div class="rule-setting"> <h3>网站访问规则设置</h3> <time-picker v-model="dailyTime" label="每日最大时长"/> <domain-block-list :domains="blockedSites" @update="updateBlockList"/> <schedule-calendar @periods-change="handlePeriodsChange"/> </div> </template> <script> // 使用ECharts展示使用情况(参考家校系统数据看板[^4]) import { getUsageData } from '@/api/accessControl'; export default { data() { return { dailyTime: 120, blockedSites: ['games.com', 'socialmedia.com'], allowedPeriods: [] } }, methods: { updateBlockList(newList) { this.$axios.put('/api/rules/blocked-sites', newList) .then(() => this.$message.success('黑名单已更新')) } } } </script> ``` ### 三、关键技术点 1. **实时时长计算**:采用WebSocket推送剩余时间(参考家校系统消息模块[^4]) 2. **访问拦截策略**: - 前端路由守卫检查剩余时长 ```javascript router.beforeEach((to, from, next) => { store.dispatch('checkRemainingTime').then(remaining => { if(remaining <= 0 && to.path !== '/timeout') { next('/timeout') } else { next() } }) }) ``` 3. **数据可视化**:复用家校系统的ECharts组件[^4],展示每日使用趋势图 ### 四、安全增强措施 1. 双因素认证(参考心理咨询系统[^2]) 2. 访问日志加密存储(AES-256) 3. 防绕过机制:同时在前端路由和后端接口进行双重校验 ### 五、部署方案 1. 使用Nginx反向代理(参考儿童教育网站部署[^3]) 2. 添加HTTPS支持 3. 配置自动化监控(Prometheus+Grafana) --- ### 相关问题 1. 如何防止孩子通过修改本地时间绕过限制? (可参考家校系统的时间同步机制[^4],结合NTP服务器校验) 2. 系统如何实现不同年龄段的差异化时长设置? (可扩展规则表添加年龄字段,参考幼教课程分类逻辑[^3]) 3. 网站拦截功能如何兼容HTTPS流量? (需使用MITM代理或浏览器扩展方案,类似家长控制软件的实现[^1]) [^1]: 幼儿园管理系统时间管理模块 [^2]: 心理咨询系统安全认证方案 [^3]: 儿童教育网站资源分类方法 [^4]: 家校系统角色权限控制实现
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bummon.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值