Mapper层
对于文章的点赞和浏览都会增加一
<update id="increaseViewCount">
update doc set view_count = view_count + 1 where id = #{id}
</update>
<update id="increaseVoteCount">
update doc set vote_count = vote_count + 1 where id = #{id}
</update>
public void increaseViewCount(@Param("id") Long id);
public void increaseVoteCount(@Param("id") Long id);
但是对于我们的点赞,肯定是不能一直点赞的,所以我们需要进行一个限制,比如一天只能点赞一次
我们就需要获取到用户的远程ip地址,把ip地址存入到redis中,当用户点赞时,我们就会去redis查询是否有,如果有就点赞,没有的话就抛出错误
一.Controller层中定义点赞接口
@GetMapping("/vote/{id}")
public CommonResp vote(@PathVariable Long id) {
CommonResp commonResp = new CommonResp();
docService.vote(id);
return commonResp;
}
二. Service中调用
/**
* 点赞
*/
public void vote(Long id) {
// docMapperCust.increaseVoteCount(id);
// 远程IP+doc.id作为key,24小时内不能重复
String ip = RequestContext.getRemoteAddr();
if (redisUtil.validateRepeat("DOC_VOTE_" + id + "_" + ip, 5000)) {
docMapperCust.increaseVoteCount(id);
} else {
throw new BusinessException(BusinessExceptionCode.VOTE_REPEAT);
}
}
三.定义工具类RequestContext开启线程,redisUtil进行存储远程IP
package com.jiawa.wiki.util;
import java.io.Serializable;
public class RequestContext implements Serializable {
private static ThreadLocal<String> remoteAddr = new ThreadLocal<>();
public static String getRemoteAddr() {
return remoteAddr.get();
}
public static void setRemoteAddr(String remoteAddr) {
RequestContext.remoteAddr.set(remoteAddr);
}
}
package com.jiawa.wiki.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
private static final Logger LOG = LoggerFactory.getLogger(RedisUtil.class);
@Resource
private RedisTemplate redisTemplate;
/**
* true:不存在,放一个KEY
* false:已存在
* @param key
* @param second
* @return
*/
public boolean validateRepeat(String key, long second) {
if (redisTemplate.hasKey(key)) {
LOG.info("key已存在:{}", key);
return false;
} else {
LOG.info("key不存在,放入:{},过期 {} 秒", key, second);
redisTemplate.opsForValue().set(key, key, second, TimeUnit.SECONDS);
return true;
}
}
}
四.在AOP中取出地址,和设置Nginx
package com.jiawa.wiki.aspect;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.jiawa.wiki.util.RequestContext;
import com.jiawa.wiki.util.SnowFlake;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/** 定义一个切点 */
@Pointcut("execution(public * com.jiawa.*.controller..*Controller.*(..))")
public void controllerPointcut() {}
@Resource
private SnowFlake snowFlake;
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
RequestContext.setRemoteAddr(getRemoteIp(request));
// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//执行前,打印开始时间
long startTime = System.currentTimeMillis();
//执行业务内容
Object result = proceedingJoinPoint.proceed();
//执行后
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
/**
* 使用nginx做反向代理,需要用该方法才能取到真实的远程IP
* @param request
* @return
*/
public String getRemoteIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
这样就完成限制每一个IP只能一天点赞一次,并且浏览书籍也会增加书籍阅读量
SpringBoot 如何执行定时任务
工作中有需要应用到定时任务的场景,一天一次,一周一次,一月一次,一年一次,做日报,周报,月报,年报的统计,以及信息提醒,等,spring boot 提供了一个两种方式实现定时任务。
在开发中,我们常常需要用到定时任务,比如对我们的图书进行浏览后,进行点赞,对于阅读数和点赞数进行一个更新
一.先写好表查询的sql(Mapper层)
<update id="updateEbookInfo">
update ebook t1, (select ebook_id, count(1) doc_count, sum(view_count) view_count, sum(vote_count) vote_count from doc group by ebook_id) t2
set t1.doc_count = t2.doc_count, t1.view_count = t2.view_count, t1.vote_count = t2.vote_count
where t1.id = t2.ebook_id
</update>
public void updateEbookInfo();
二.在我们的Service中定义
public void updateEbookInfo() {
docMapperCust.updateEbookInfo();
}
三.接下来就是写我们重要的内容
定时任务
@Component
public class DocJob {
private static final Logger LOG = LoggerFactory.getLogger(DocJob.class);
@Resource
private DocService docService;
/**
* 每30秒更新电子书信息
*/
@Scheduled(cron = "5/30 * * * * ?")
public void cron() {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
LOG.info("更新电子书下的文档数据开始");
long start = System.currentTimeMillis();
docService.updateEbookInfo();
LOG.info("更新电子书下的文档数据结束,耗时:{}毫秒", System.currentTimeMillis() - start);
}
}
通过@Commpent注解,将注入到IOC容器里
导入Logback日志
通过@Scheduled注解开启定时任务
利用cron设置定时时间
我们前端页面就可以显示出来,我们每一段时间就会自动更新Sql,并且吧数据显示到前端上
集成WebSocket把点赞通知显示到前端页面上
功能:网站通知
点暂时,前端收到通知
定时轮询&被动通知
一.导入websocket
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
二.配置Config
package com.jiawa.wiki.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
三.新建Websocket类用于开启websocket的连接
package com.jiawa.wiki.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
@Component
@ServerEndpoint("/ws/{token}")
public class WebSocketServer {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class);
/**
* 每个客户端一个token
*/
private String token = "";
private static HashMap<String, Session> map = new HashMap<>();
/**
* 连接成功
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
map.put(token, session);
this.token = token;
LOG.info("有新连接:token:{},session id:{},当前连接数:{}", token, session.getId(), map.size());
}
/**
* 连接关闭
*/
@OnClose
public void onClose(Session session) {
map.remove(this.token);
LOG.info("连接关闭,token:{},session id:{}!当前连接数:{}", this.token, session.getId(), map.size());
}
/**
* 收到消息
*/
@OnMessage
public void onMessage(String message, Session session) {
LOG.info("收到消息:{},内容:{}", token, message);
}
/**
* 连接错误
*/
@OnError
public void onError(Session session, Throwable error) {
LOG.error("发生错误", error);
}
/**
* 群发消息
*/
public void sendInfo(String message) {
for (String token : map.keySet()) {
Session session = map.get(token);
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
LOG.error("推送消息失败:{},内容:{}", token, message);
}
LOG.info("推送消息:{},内容:{}", token, message);
}
}
}
四.更改Service层代码
/**
* 点赞
*/
public void vote(Long id) {
// docMapperCust.increaseVoteCount(id);
// 远程IP+doc.id作为key,24小时内不能重复
String ip = RequestContext.getRemoteAddr();
if (redisUtil.validateRepeat("DOC_VOTE_" + id + "_" + ip, 5000)) {
docMapperCust.increaseVoteCount(id);
} else {
throw new BusinessException(BusinessExceptionCode.VOTE_REPEAT);
}
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
String logId = MDC.get("LOG_ID");
wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);
// rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docDb.getName() + "】被点赞!");
}
查询出书本的ID,利用ID查询出名字,来进行通知