首发公众号:MarkerHub
作者:吕一明
原文链接:https://www.zhuawaba.com/post...
线上演示地址:https://www.zhuawaba.com/dail...
视频解说:https://www.bilibili.com/vide...
源码地址:请关注公众号:Java问答社,回复【678】获取
1、前言
咱们常常浏览很多网页,看到一些感觉有用、或者有意思的网页时候,咱们通常会珍藏到书签。然而当书签的珍藏越来越多,分类越来越多,想找到之前的那条珍藏就比拟麻烦,尽管也有搜寻性能,但还须要另外点击很多操作。
最重要的是,珍藏网页的时候我往往须要记录一些浏览心得,作为我浏览的脚印和记忆。其实对我来说,珍藏的分类不是特地重要,这是一个费脑的过程,因为很多网页能够放到多个文件夹,这时候又呈现了抉择艰难症了,网页各式各样,总不能给每种网页都起个分类珍藏。对我来说有点冗余。
于是我打算开发一个零碎,以工夫为记录线,在未关上网站的时候就能够疾速记录我以后浏览网页的网址和题目,而后我还能够记录心得。另外还须要一个很弱小的搜索引擎,疾速搜寻记录。这样我能够查看我每天浏览了那些网页,而后还能够分享到珍藏广场上给更多的网友。
那么,接下来,跟着我,一起去实现这个我的项目的开发吧
我的项目性能
- 公众号扫码登录注册
- 疾速珍藏网页
- 收藏夹列表
- 珍藏检索
技术栈
后端:springboot、spring data jpa、mysql、redis、elasticsearch、canal、mapstruct
前端:bootstrap 5
其实之前我在eblog我的项目中做个搜寻性能,那时候应用的是rabbitmq同步数据到es,这次我为了缩小代码开发的量,应用了canal基于binlog同步数据到es,这波及到服务搭建的过程,后续我都会一一解说。
2、线上演示
https://www.zhuawaba.com/dailyhub
3、新建springboot我的项目,整合jpa、freemarker
关上IDEA开发工具,咱们先来新建一个springboot我的项目,很惯例的操作,项目名称dailyhub,咱们把须要的jar间接引入,比方jpa、redis、mysql、lombok、dev调试。
新建我的项目
maven导入相干的jar,本来我是想做一个前后端拆散我的项目的,起初想想话太多工夫在前端,我又不太想了,于是我应用了freemarker作为模板引擎。
我的项目初建
对了,因为常常用到一些工具类,我喜爱用hutool,所以记得提前引入哈:
- pom.xml
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.16</version></dependency>
接下来,咱们整合jpa和freemarker,让我的项目能够拜访数据库和展现页面内容。
整合jpa
jpa的整合及其简略,咱们只须要配置数据源的信息,连贯上数据库,其余的整合工作都曾经帮咱们配置好的了。
- application.yml
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost/dailyhub?useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2B8 username: root password: admin jpa: database: mysql show-sql: true hibernate: ddl-auto: update
下面配置中,记得要去新建一个dailyhub的数据库哇,因为后续的用户名称可能会有头像等特殊字符,所以新建数据库字符集记得要用utf8mb4的格局哈。
而后因为是jpa,表和字段信息在我的项目启动时候会随着你定义的bean类属性信息主动创立。所以咱们不须要手动去建表。
为了测试,咱们先来定义用户表信息,我打算通过用户扫描二维码形式实现登录,所以记录的信息不多,我也不须要收集太多用户信息,所以字段非常简单。
com.markerhub.entity.User
@Data@Entity@Table(name = "m_user")public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; // 头像 private String avatar; // 微信用户身份id @JsonIgnore private String openId; // 上次登录 private LocalDateTime lasted; private LocalDateTime created; private Integer statu;}
而后接下来新建UserRepository,当然了,因为咱们是我的项目实战,所以要求你须要有点jpa的常识哈。UserRepository继承JpaRepository,JpaRepository是SpringBoot Data JPA提供的十分弱小的根底接口,领有了根本CRUD性能以及分页性能。
- com.markerhub.repository.UserRepository
public interface UserRepository extends JpaRepository<User, Long> { User findByOpenId(String openId);}
而后咱们来定义一个测试controller,因为小我的项目,我不想在test中测试了。
- com.markerhub.controller.TestController
@Controllerpublic class TestController { @Autowired UserRepository userRepository; @ResponseBody @GetMapping("/test") public Object test() { return userRepository.findAll(); }}
我的项目启动之后,零碎会主动创立表信息,而后咱们手动增加一条数据进去。而后调用http://localhost:8080/test 接口,咱们就能返回user表中的所有数据了。
因为openid字段我增加了@JsonIgnore,所以在返回的json序列号字符串中,咱们是看不到的。这也是为了暗藏要害敏感信息。
那么到这里,jpa咱们就曾经整合胜利了,接下来咱们来说一下freemarker。
整合Freemarker
在新版本的freemarker中,后缀曾经批改成了.ftlh,为了不便和习惯,我又改回.ftl,而后为了解决页面呈现空值时候会报错,所以须要设置classic_compatible信息,那么配置如下:
application.yml
spring:freemarker: suffix: .ftl settings: classic_compatible: true
而后在templates目录下新建test.ftl文件:
templates/test.ftl
<p>你好,${user.username}, 这里是dailyhub!</p>
后端咱们须要把用户的信息传过来,所以定义后端接口:
com.markerhub.controller.TestController#ftl
@GetMapping("/ftl")public String ftl(HttpServletRequest req) { req.setAttribute("user", userRepository.getById(1L)); return "test";}
拜访http://localhost:8080/ftl,后果如下:
4、对立后果封装
每做一个我的项目,都绕不开的util类,后果封装,为了让ajax申请的数据有个对立的格局,所以咱们须要封装一个对立的后果类,能够一下子就能看出申请后果是否失常等。
com.markerhub.base.lang.Result
@Datapublic class Result<T> implements Serializable { public static final int SUCCESS = 0; public static final int ERROR = -1; private int code; private String mess; private T data; public Result(int code, String mess, T data) { this.code = code; this.mess = mess; this.data = data; } public static <T> Result<T> success() { return success(null); } public static <T> Result<T> success(T data) { return new Result<>(SUCCESS, "操作胜利", data); } public static <T> Result<T> failure(String mess) { return new Result<>(ERROR, mess, null); }}
这里我用到了泛型,也是为了返回后果的时候限定返回某种类型,而不是随便的一个Object,防止数据返回不统一等问题。
5、全局异样解决
之前在vueblog和vueadmin两个我的项目中,全局异样解决我都喜爱用注解@ControllerAdvice+@ExceptionHandler来解决异样,这次咱们应用另外一种形式,咱们还能够通过继承HandlerExceptionResolver,通过重写resolveException来解决全局的异样。
com.markerhub.base.exception.GlobalExceptionHandler
@Slf4j@Componentpublic class GlobalExceptionHandler implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (ex instanceof IllegalArgumentException || ex instanceof IllegalStateException) { log.error(ex.getMessage()); } else { log.error(ex.getMessage(), ex); } // ajax申请 String requestType = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equals(requestType)) { try { response.setContentType("application/json;charset=UTF-8"); response.getWriter().print(JSONUtil.toJsonStr(Result.failure(ex.getMessage()))); } catch (IOException e) { // do something } return new ModelAndView(); } else { request.setAttribute("message", "零碎异样,请稍后再试!"); } return new ModelAndView("error"); }}
留神IllegalArgumentException等信息通常都是业务校验信息是否失常,所以个别咱们不会在日志中打印异样的具体信息,间接打印异样音讯即可。而后碰到的是ajax申请时候,咱们返回的是Result对立封装后果的json字符串。否则就是返回error.ftl页面,输入错误信息。所以咱们在templates目录下新建error.ftl页面,等前面咱们能够重写报错页面,当初能够简略点:
- templates/error.ftl
6、公众号扫码登录性能开发
其实我做这个性能就是为了给公众号引流,让用户拜访我网址时候能够顺便关注我的公众号,达到涨粉的目标,我相似的网站还有https://zhuawaba.com/login ,我的公众号都是认证的企业订阅号,不晓得集体号可不可以,这个还待确认,如果须要集体号应用这个性能能够本人去官网查看一下相干的接口哈。
扫码原理
原理阐明:
- 用户发动登录申请
- 服务端生成code、ticket返回前端
- 前端开始每3秒循环拜访后端,携带code和ticket
- 用户扫码公众号,并在公众号上回复code
- 微信端接管到用户输出关键字,返回关键字和openid到指定的配置后端接口
- 后端接管到微信端的回调,应用openid获取用户信息,对用户进行注册解决(新用户),而后把用户信息存入redis中,code作为key。
前端循环拜访时候发现后端redis中曾经有用户信息,验证码code和ticket是否匹配,匹配胜利之后,后端在session中存入用户信息,用户登录胜利,前端跳转到首页。
登录页面
因为不是前后端拆散的我的项目,所以个别我都喜爱先把页面写好,而后须要什么数据我再填充,这样省略一些接口调试的工夫。我应用了bootstrap 5的页面款式框架,留神同步哈。
依据下面的扫码逻辑,咱们在登录页面须要的是一个公众号的二维码,还有登录的验证码,所以页面就绝对比较简单了。
- templates/login.ftl
<body class="text-center"><main class="form-signin"> <form> <img class="mb-4" src="/images/logo.jpeg" alt="" width="72" height="72" style="border-radius: 15%;"> <h1 class="h3 mb-3 fw-normal">浏览珍藏 - dailyhub</h1> <img src="/images/javawenda.jpeg" alt="公众号:Java问答社"> <div class="mt-2 mb-2 text-muted"> 登录验证码: <strong style="background-color: yellow; padding: 2px; font-size: 18px;" id="qrcodeeeee"> ${code} </strong> </div> <p class="text-muted">扫码关注公众号,回复上方验证码登录</p> </form></main> var dingshi = setInterval(function () { $.get('/login-check' ,{ code: '${code}', ticket: '${ticket}', }, function (res) { console.log(res) if(res.code == 0) { location.href = "/"; } }); }, 3000); setTimeout(function () { clearInterval(dingshi); console.log("已敞开定时器~") $("#qrcodeeeee").text("验证码过期,请刷新!"); }, 180000); </body></body></html>
最终成果:
因为登录验证码是有有效期的,所以我定义了一个js定时器,当超过3分钟的时候,主动把验证码切换成已过期的状态。另外为了验证用户是否曾经等了,每3秒就去拜访一下服务器,这里我没有应用websocket,这个申请尽管频繁,但不须要查库,对服务器压力也不是特地大。当然了,你也能够应用ws的形式。
验证码过期成果:
获取登录验证码
而后在服务段,其实就比较简单了,就把登录验证码生成而后传到前端就行:
- com.markerhub.controller.LoginController#login
/** * 1、获取验证码 */@GetMapping(value = "/login")public String login(HttpServletRequest req) { String code = "DY" + RandomUtil.randomNumbers(4); while (redisUtil.hasKey(code)) { code = "DY" + RandomUtil.randomNumbers(4); } String ticket = RandomUtil.randomString(32); // 5 min redisUtil.set(code, ticket, 5 * 60); req.setAttribute("code", code); req.setAttribute("ticket", ticket); log.info(code + "---" + ticket); return "login";}
随机生成DY结尾的登录验证码code以及校验用的ticket(避免他人伪造登录验证码暴力拜访),保留到redis中,而后返回前端。
前端把登录验证码展现给用户,用户扫码公众号二维码,而后输出登录验证码。
微信端接管到用户输出的关键字之后,把用户输出的内容一成不变返回,同时还回调返回openid以及一些用户相干信息。回调链接时咱们提前设置在公众号设置中的哈。
openid将作为用户的密钥信息,后续咱们判断用户是谁都是通过openid,所以务必要妥善保留。其实为了隐衷,能够加密存库。
循环申请登录后果
当用户关上登录页面的时候,页面就会发动一个每3秒一次的登录后果申请,历时3分钟,当发现用户曾经发送登录验证码到公众号的时候就会主动跳转到首页。
- com.markerhub.controller.LoginController#loginCheck
/** * 验证code是否曾经实现登录 */@ResponseBody@GetMapping("/login-check")public Result loginCheck(String code, String ticket) { if(!redisUtil.hasKey("Info-" + code)) { return Result.failure("未登录"); } String ticketBak = redisUtil.get(code).toString(); if (!ticketBak.equals(ticket)) { return Result.failure("登录失败"); } String userJson = String.valueOf(redisUtil.get("Info-" + code)); UserDto user = JSONUtil.toBean(userJson, UserDto.class); req.getSession().setAttribute(Const.CURRENT_USER, user); return Result.success();}
能够看到,这个查看业务比价简略,就查看redis中是否曾经有了对应的key,而后通过key获取对应的登录用户信息,而后存储到session中,实现用户登录。
整合WxJava
为了后续的公众号业务解决不便些,咱们这里引入一个公众号开发整合包WxJava。
其实扫码登录曾经波及到了公众号开发,所以咱们应用一个工具来帮咱们简化一些开发工具,这里咱们抉择应用WxJava,
- 码云:https://gitee.com/binary/weixin-java-tools
- GitHub:https://github.com/wechat-group/WxJava
对应公众号开发相干的文档:https://github.com/Wechat-Group/WxJava/wiki/公众号开发文档,咱们要解决的首先就是同步回复音讯的接口。
当初版本更新很快,我以前应用的是3.2.0版本,这里我就不应用最新版本了。
- pom.xml
<!--微信公众号开发 https://github.com/Wechat-Group/WxJava--><dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-mp</artifactId> <version>3.2.0</version></dependency>
而后咱们须要配置微信公众号的密钥信息,而后初始化WxMpConfigStorage和WxMpService这两个类,这样咱们就能够失常应用wxjava的所有api了。
- WxMpService:微信API的Service
- WxMpConfigStorage:公众号客户端配置存储
创立config包,而后新建一个com.markerhub.config.WeChatMpConfig配置类。
- com.markerhub.config.WeChatMpConfig
@Data@Slf4j@Configuration@ConfigurationProperties(prefix = "wechat")public class WeChatMpConfig { private String mpAppId; private String mpAppSecret; private String token; @Bean public WxMpService wxMpService() { WxMpService wxMpService = new WxMpServiceImpl(); wxMpService.setWxMpConfigStorage(wxMpConfigStorage()); return wxMpService; } /** * 配置公众号密钥信息 * @return */ @Bean public WxMpConfigStorage wxMpConfigStorage() { WxMpInMemoryConfigStorage wxMpConfigStorage = new WxMpInMemoryConfigStorage(); wxMpConfigStorage.setAppId(mpAppId); wxMpConfigStorage.setSecret(mpAppSecret); wxMpConfigStorage.setToken(token); return wxMpConfigStorage; } /** * 配置音讯路由 * @param wxMpService * @return */ @Bean public WxMpMessageRouter router(WxMpService wxMpService) { WxMpMessageRouter router = new WxMpMessageRouter(wxMpService); // TODO 音讯路由 return router; }}
- WxMpMessageRouter:微信音讯路由器,通过代码化的配置,把来自微信的音讯交给handler解决
代码中token、mpAppId和mpAppSecret等信息都是在公众号中取得的,如果你没有公众号,咱们能够去微信公众平台接口测试帐号申请,网址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login, 登录之后,就能够失去一个测试号相干的mpAppId和mpAppSecret信息了,而后token和URL是须要本人设置的。Token能够随便,放弃和代码始终就行,URL是须要一个内网穿透工具了,我应用的是natapp.cn这个工具,映射到本地的8080端口,这样外围就能够拜访我本地的接口了。 - 测试环境配置:
- 线上环境配置:
而后咱们把密钥的信息配置到application.yml文件中:
- application.yml
wechat: mpAppId: wxf58aec8********5 mpAppSecret: efacfe9b7c1b*************c954 token: 111111111
微信音讯回调
当咱们在配置的公众号输出内容音讯时候,公众平台就会回调咱们配置链接,把用户输出的内容发送给咱们的后盾,所以咱们这里须要做内容的接管与解决,而后把处理结果返回给公众平台。
- com.markerhub.controller.LoginController#wxCallback
/** * 服务号的回调 */@ResponseBody@RequestMapping(value = "/wx/back")public String wxCallback(HttpServletRequest req, HttpServletResponse resp) throws IOException { String signature = req.getParameter("signature"); String timestamp = req.getParameter("timestamp"); String nonce = req.getParameter("nonce"); String echoStr = req.getParameter("echostr");//用于验证服务器配置 if (StrUtil.isNotBlank(echoStr)) { log.info("---------------->验证服务器配置"); return echoStr; } if (!wxService.checkSignature(timestamp, nonce, signature)) { // 音讯不非法 log.error("------------------> 音讯不非法"); return null; } String encryptType = StringUtils.isBlank(req.getParameter("encrypt_type")) ? "raw" : req.getParameter("encrypt_type"); WxMpXmlMessage inMessage = null; if ("raw".equals(encryptType)) { // 明文传输的音讯 inMessage = WxMpXmlMessage.fromXml(req.getInputStream()); } else if ("aes".equals(encryptType)) { // 是aes加密的音讯 String msgSignature = req.getParameter("msg_signature"); inMessage = WxMpXmlMessage.fromEncryptedXml(req.getInputStream(), wxMpConfigStorage, timestamp, nonce, msgSignature); } else { log.error("-------------> 不可辨认的加密类型 {}" + encryptType); return "不可辨认的加密类型"; } // 路由到各个handler WxMpXmlOutMessage outMessage = wxMpMessageRouter.route(inMessage); log.info("返回后果 ----------------> " + outMessage); String result = outMessage == null ? "" : outMessage.toXml(); return result;}
从下面的代码能够看出,其实不是很简单,都是为了验证音讯是否非法,真正有用的代码是这行:
// 路由到各个handlerWxMpXmlOutMessage outMessage = wxMpMessageRouter.route(inMessage);
这个咱们用到了wxjava的路由概念,咱们须要提前配置号路由规定,比方当用户输出的是文字或者图片、语音等内容时候,咱们须要路由到不同的处理器来解决音讯内容。上面咱们设置一下路由以及处理器。
字符串处理器
而后依据wxjava官网的阐明:
咱们须要解决的次要是文本音讯,所以咱们在路由中配置一个解决文本音讯的处理器TextHandler。
所以,咱们在WeChatMpConfig中增加一个同步文本音讯路由:
- com.markerhub.config.WeChatMpConfig#router
@AutowiredTextHandler textHandler;@Beanpublic WxMpMessageRouter router(WxMpService wxMpService) { WxMpMessageRouter router = new WxMpMessageRouter(wxMpService); // TODO 音讯路由 router .rule() .async(false) .msgType(WxConsts.XmlMsgType.TEXT) .handler(textHandler) .end(); return router;}
有了下面的配置,当用户在公众号回复文本类型的字符串时候,就会路由到textHandler解决信息。并且设置了时同步回复。
而后咱们来定义TextHandler,咱们须要实现WxMpMessageHandler接口重写handle接口。
登录的字符串咱们定义成【DY + 4位随机数字】的格局,所以当公众号收到DY结尾的字符串时候,咱们就当成是用户登录的凭证来解决。这时候咱们独自定义一个LoginHandler类,集中把登录解决的业务写在外面。防止前面更多业务的时候代码太多。
- com.markerhub.handler.TextHandler
@Slf4j@Componentpublic class TextHandler implements WxMpMessageHandler { private final String UNKNOWN = "未辨认字符串!"; @Autowired LoginHandler loginHandler; @Override public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException { String openid = wxMessage.getFromUser(); String content = wxMessage.getContent(); String result = UNKNOWN; if (StrUtil.isNotBlank(content)) { content = content.toUpperCase().trim(); // 解决登录字符串 if (content.indexOf("DY") == 0) { result = loginHandler.handle(openid, content, wxMpService); } } return WxMpXmlOutMessage.TEXT() .content(result) .fromUser(wxMessage.getToUser()) .toUser(wxMessage.getFromUser()) .build(); }}
能够看到,遇到DY结尾登录字符串交给LoginHandler解决其余音讯一律返回未辨认。
- LoginHandler
@Slf4j@Componentpublic class LoginHandler { @Value("${server.domain}") String serverDomain; @Autowired RedisUtil redisUtil; @Autowired UserService userService; public String handle(String openid, String content, WxMpService wxMpService) { String result; if (content.length() != 6 || !redisUtil.hasKey(content)) { return "登录验证码过期或不正确!"; } // 解决手机端登录 String token = UUID.randomUUID().toString(true); String url = serverDomain + "/autologin?token=" + token ; WxMpUser wxMapUser = new WxMpUser(); result = "欢送你!" + "\n\n" + "<a href='" + url + "'>点击这里实现登录!</a>"; // 注册操作 UserDto user = userService.register(wxMapUser); // 用户信息存在redis中5分钟 redisUtil.set("Info-" + content, JSONUtil.toJsonStr(user), 5 * 60); // 手机端登录 redisUtil.set("autologin-" + token, JSONUtil.toJsonStr(user), 48 * 60 * 60); return result; }}
LoginHandler中次要做了几件事件:
- 验证登录验证码是否存在和失常
- 应用openid获取用户信息
- 用户注册
- 把用户信息保留到redis中
- 生成随机tonken,不便手机端登录操作
在用户注册register办法中,咱们须要做如下存库解决:
- com.markerhub.service.impl.UserServiceImpl#register
@Servicepublic class UserServiceImpl implements UserService { @Autowired UserRepository userRepository; @Autowired UserMapper userMapper; @Override @Transactional public UserDto register(WxMpUser wxMapUser) { String openId = wxMapUser.getOpenId(); User user = userRepository.findByOpenId(openId); if (user == null) { user = new User(); String avatar = "https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg"; user.setAvatar(avatar); user.setUsername("Hub-" + RandomUtil.randomString(5)); user.setCreated(new Date()); user.setLasted(new Date()); user.setOpenId(openId); user.setStatu(Const.STATUS_SUCCESS); } else { user.setLasted(new Date()); } userRepository.save(user); log.info("用户注册成:------{}", user.getUsername()); UserDto userDto = userMapper.toDto(user); return userDto; }}
在2021年12月27日开始,公众号官网调整了用户信息接口,曾经获取不到了头像和昵称信息,所以这里我就间接写死了,后续能够提供批改材料页面让用户自行批改,这还是挺简略的。
- com.markerhub.base.dto.UserDto
@Datapublic class UserDto implements Serializable { private Long id; private String username; private String avatar; private LocalDateTime lasted; private LocalDateTime created;}
应用mapstruct
保留用户信息之后返回User,须要转成UserDto,这里我应用了一个框架mapstruct,真心觉的好用呀。
对于它的介绍,能够去官网看一下:https://mapstruct.org/
MapStruct的原理是生成和咱们本人写的代码一样的代码,这意味着这些值是通过简略的getter/setter调用而不是反射或相似的办法从source类复制到target类的。使得MapStruct的性能会比动静框架更加优良。这其实和lombok其实有点相似。
首先咱们须要导入mapstruct包。须要留神的是,如果我的项目中有引入lombok,须要解决一下抵触问题哈,plugin像我那样配置一下就行了。
- pom.xml
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.4.2.Final</version></dependency>// plugins中增加上面<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <!-- depending on your project --> <target>1.8</target> <!-- depending on your project --> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.2.Final</version> </path> <!--为了解决lombok抵触问题--> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </path> <!-- other annotation processors --> </annotationProcessorPaths> </configuration></plugin>
而后咱们须要做User与UserDto之间的转换,咱们能够新建一个UserMapper,而后
- com.markerhub.mapstruct.UserMapper
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)public interface UserMapper { UserDto toDto(User user);}
而后属性雷同的字段就能映射过来了,不同属性的话,能够通过注解来调整,后续咱们也会用到。
而后咱们能够看一下生成的映射代码:
手机端登录验证
之前为了让手机端可能实现登录,咱们生成了一个token作为key存储以后用户的登录信息,而后咱们须要把这个token返回给用户的手机端。
具体操作就是当用户输出登录验证码时候,咱们返回手机端的登录链接给用户,同时pc端的网页主动跳转到首页实现主动登录。
拜访登录页面,在公众号中回复登录验证码,失去的成果如下:
而后pc端的登录页面会主动调跳转到首页。首页的链接咱们还没配置,等下弄。
而后在手机端,咱们能够点击提醒的【点击这里实现登录】来实现手机端的登录操作,其实链接是这样的:http://localhost:8080/autologin?token=4eee0effa21149c68d4e95f8667cef49
服务端曾经绑定了token与以后用户的信息,所以能够应用该token实现登录操作。
- com.markerhub.controller.LoginController#autologin
/** * 手机端登录 */@GetMapping("/autologin")public String autologin(String token) { log.info("-------------->" + token); String userJson = String.valueOf(redisUtil.get("autologin-" + token)); if (StringUtils.isNotBlank(userJson)) { UserDto user = JSONUtil.toBean(userJson, UserDto.class); req.getSession().setAttribute(Const.CURRENT_USER, user); } return "redirect:/index";}// 登记@GetMapping("/logout")public String logout() { req.getSession().removeAttribute(Const.CURRENT_USER); return "redirect:/index";}
内网穿透
好了,实现了下面代码之后咱们就能够进行扫码登录了,记得须要配置内网映射工具的穿透。比方我的:
我的回调地址也设置成了 http://yimin.natapp1.cc/wx/back ,只有这样微信回调能力拜访到我本地的测试环境哈。
7、登录与权限拦挡
自定义@Login注解
并不是所有的链接都能随便拜访的,所以须要做一个登录认证。这里为了不便,让我的项目架构更加轻便一些,我没有应用shiro或者spring security等权限框架。而是打算间接应用一个拦截器搞定,毕竟这里只须要做个简略的登录拦挡就行,没波及到权限的问题。
其实不写做个@Login注解也是能够的,不过思考到当前新增接口时候不遗记配置登录认证,索性就加了,注解很简略,在须要登录认证能力拜访的接口上方标识这个注解就行了。
- com.markerhub.base.annotation.Login
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface Login {}
编写登录拦截器
登录拦截器的逻辑很简略,首先须要判断用户是否曾经登录,而后判断申请的接口是否有@Login注解,如果有而且用户未登录就重定向到登录接口;否则就放行。
- com.markerhub.interceptor.AuthInterceptor
@Slf4jpublic class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserDto userDto = (UserDto)request.getSession().getAttribute(Const.CURRENT_USER); if (userDto == null) { userDto = new UserDto(); userDto.setId(-1L); } request.setAttribute("current", userDto); Login annotation; if(handler instanceof HandlerMethod) { annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class); }else{ return true; } if(annotation == null){ // 没有@Login注解,阐明是公开接口,间接放行 return true; } if (userDto.getId() == null || userDto.getId() == -1L) { response.sendRedirect("/login"); return false; } log.info("欢迎您:{}", userDto.getUsername()); return true; }}
而后咱们须要配置一下拦截器,注入到springboot中,这个比较简单,学过springboot的人都应该懂:
- com.markerhub.config.WebConfig
@Configurationpublic class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**") .excludePathPatterns( "/js/**" , "/css/**" , "/images/**" , "/layui/**" ); }}
@Login作用于办法上,在须要登录认证的中央增加此注解就行了。成果啥的我就不截图展现了,应该都能设想到。
作者:吕一明
原文链接:https://www.zhuawaba.com/post...
线上演示地址:https://www.zhuawaba.com/dail...
视频解说:https://www.bilibili.com/vide...
源码地址:https://github.com/MarkerHub/...
8、我的珍藏
实体设计
咱们先来设计一个实体类,利用于存储对应的珍藏记录,其实这个实体还是很简略的,珍藏次要的属性就几个:
- id
- userId - 珍藏用户
- title - 题目
- url - 珍藏对应的链接
- note - 笔记、珍藏想法
- personal - 是否仅自己可见
而后咱们再加上一些必要的创立工夫啥的,就能够了。因为是spring data jpa,咱们须要一对多的这些关系,珍藏对于用户来说,是多对一的关系。所以实体类能够设计成这样:
- com.markerhub.entity.Collect
@Data@Entity@Table(name = "m_collect")public class Collect implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String url; @ManyToOne @JoinColumn(name = "user_id") private User user; // 笔记想法 private String note; // 是否公开,0公开,1公有,默认公开 private Integer personal = 0; // 珍藏日期,不存工夫局部 private LocalDate collected; private LocalDateTime created;
而后我的项目启动之后,表构造会主动生成。对应到页面的Dto,能够是这样:
- com.markerhub.base.dto.CollectDto
@Datapublic class CollectDto implements Serializable { private Long id; private String title; private String url; private String note; // 是否公开,0公开,1公有,默认公开 private Integer personal = 0; // 珍藏日期 private LocalDate collected; private LocalDateTime created; private UserDto user;}
公共页面抽取
接下来,咱们实现零碎的次要业务性能,珍藏性能。首先我还是喜爱先去实现页面,我的珍藏就是整个零碎的首页。所以我在templates下新建index.ftl页面。因为每个页面都有公共的援用或者雷同的组件局部,所以咱们利用freemarker的宏的概念,定义每个页面的模块内容,把公共局部抽取进去。
于是抽取进去之后的公共模板内容是这样的:
- /inc/layout.ftl
<#macro layout title> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>${title} - dailyhub</title> ...js 和 css </head> <body> <div class="container" style="max-width: 960px;"> <#include "/inc/header.ftl" /> <#nested> </div> $(function () { layui.config({ version: false , debug: false , base: '' }); }); </body> </html></#macro>
macro 标签就是用来定义宏的
nested 援用标签的内容主体地位。
咱们看两头的body局部,include引入了header.ftl页面,这个页面我就不贴代码了贴个图吧,就是logo、顶部导航、搜寻框、以及登录按钮或用户头像信息等。
- /inc/header.ftl
头部成果如下:
页面排版
其实我css并不是很强,所以我都喜爱在boostrap(https://v5.bootcss.com/)上找模板套进来。
对于首页:我的珍藏的排版,我的构思是这样的,上方是导航,下方右边是用户所有珍藏的日期列表,左边是珍藏列表。
右边的珍藏日期列表每个人都是不一样的,它应该是你所有珍藏列表整合进去的一个无反复的日期列表,所以这里须要从库中查问进去并且去重。
右边打算用一个列表,我看中了bootstrap5中的这个https://v5.bootcss.com/docs/examples/cheatsheet/,于是间接把内容抽取过去。
右侧的话,能够应用一个瀑布流的卡片,我看中了这个:https://v5.bootcss.com/docs/examples/masonry/。成果是这样的:
整合之后,失去的页面代码如下:
- index.ftl
<#import 'inc/layout.ftl' as Layout><@Layout.layout "我的珍藏"> <div id="app" class="row justify-content-md-center"> <#--侧边日期--> <div class="col col-3"> <div class="flex-shrink-0 p-3 bg-white" style="width: 280px;"> <ul class="list-unstyled ps-0"> <#list datelines as dateline> <li class="mb-1"> <button class="dateline btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#collapse-${dateline.title}" aria-expanded="true"> ${dateline.title} </button> <div class="collapse show" id="collapse-${dateline.title}"> <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> <#list dateline.children as child> <li><a href="hendleDateline('${child.title}')" class="link-dark rounded">${child.title}</a></li> </#list> </ul> </div> </li> </#list> </ul> </div> </div> <!----> <div class="col col-9" id="collects-col"> <#include "/inc/collect-tpl.ftl"> <div class="row" id="masonry"></div> </div> </div> // ...js </@Layout.layout>
因为多个中央须要用到珍藏卡片,所以提取进去作为一个独自模块,而后又因为前面我想弄成js瀑布流的模式,所以须要定义模板。于是就有了这卡片模板。
- /inc/collect-tpl.ftl
{{# layui.each(d.content, function(index, item){ }} <div class="col-sm-6 col-lg-6 mb-4 masonry-item" id="masonry-item-{{item.id}}"> <div class="card p-3"> <div class="card-body"> <blockquote class="blockquote"> {{# if(item.personal == 1){ }} <span class="badge bg-info text-dark">公有</span> {{# } }} <a target="_blank" class="text-decoration-none" href="{{item.url}}"><span class="card-title text-black">{{ item.title }}</span></a> </blockquote> <p class="card-text text-muted"> <img src="{{item.user.avatar}}" alt="mdo" width="32" height="32" class="rounded-circle"> <span>{{ item.user.username }}</span> {{# if(item.user.id == ${current.id}){ }} <a class="text-reset" href="/collect/edit?id={{item.id}}">编辑</a> <a class="text-reset" href="handleDel({{item.id}})">删除</a> {{# } }} </p> <p class="card-text text-muted">{{ item.note }}</p> <figcaption class="blockquote-footer mb-0 text-muted text-end"> {{ item.collected }} </figcaption> </div> </div> </div> {{# }); }}
页面模板的编写和渲染都是依照layui的格局来的,所以能够提前去layui的网站下来理解一下layui定义页面模板。
日期侧边栏加载
首先是须要一个controller跳转到页面,须要编写IndexController
- com.markerhub.controller.IndexController
@Slf4j@Controllerpublic class IndexController extends BaseController{ @Login @GetMapping(value = {"", "/"}) public String index() { // 工夫线 List<DatelineDto> datelineDtos = collectService.getDatelineByUserId(getCurrentUserId()); req.setAttribute("datelines", datelineDtos); // 为了搜寻本人的珍藏 req.setAttribute("userId", getCurrentUserId()); return "index"; } }
其中DatelineDto是有上下级关系的,所以写成这样:
- com.markerhub.base.dto.DatelineDto
@Datapublic class DatelineDto { private String title; private List<DatelineDto> children = new ArrayList<>();}
而后collectService.getDatelineByUserId就是为了筛选出用户的所有珍藏的日期汇合。获取用户珍藏日期列表的步骤如下:
- 查询数据库,获取用户所有珍藏的所有日期,并且去重。
- 日期依照(XXXX年XX月XX日)格局,下级的格局就是(XXXX年XX月)
- 把月份雷同的日期主动排到一起
- com.markerhub.service.impl.CollectServiceImpl
@Servicepublic class CollectServiceImpl implements CollectService { @Autowired CollectRepository collectRepository; /** * 获取用户的珍藏日期列表 */ @Override public List<DatelineDto> getDatelineByUserId(long userId) { List<Date> collectDates = collectRepository.getDateLineByUserId(userId); List<DatelineDto> datelineDtos = new ArrayList<>(); for (Date date : collectDates) { // 获取下级、以后日期的题目 String parent = DateUtil.format(date, "yyyy年MM月"); String title = DateUtil.format(date, "yyyy年MM月dd日"); datelineDtos = handleDateline(datelineDtos, parent, title); } return datelineDtos; } /** * 如果下级存在就间接增加到子集中,如果不存在则新建父级再增加子集 */ private List<DatelineDto> handleDateline(List<DatelineDto> datelineDtos, String parent, String title) { DatelineDto dateline = new DatelineDto(); dateline.setTitle(title); // 查找是否有下级存在 Optional<DatelineDto> optional = datelineDtos.stream().filter(vo -> vo.getTitle().equals(parent)).findFirst(); if (optional.isPresent()) { optional.get().getChildren().add(dateline); } else { // 没有下级,那么就新建一个下级 DatelineDto parentDateline = new DatelineDto(); parentDateline.setTitle(parent); // 并且把本人增加到下级 parentDateline.getChildren().add(dateline); // 下级增加到列表中 datelineDtos.add(parentDateline); } return datelineDtos; }}
- com.markerhub.repository.CollectRepository
public interface CollectRepository extends JpaRepository<Collect, Long>, JpaSpecificationExecutor<Collect> { @Query(value = "select distinct collected from m_collect where user_id = ? order by collected desc", nativeQuery = true) List<Date> getDateLineByUserId(long userId);}
留神distinct去重哈。得进去的成果如下:
瀑布流数据加载
而后两头内容局部,咱们应用layui的瀑布流数据加载。
- 非官方网站阐明:http://bmxpx.com/modules/flow.html
页面的js局部: - /index.ftl
var userId = '${userId}' if (userId == null || userId == '') { userId = '${current.id}' } var laytpl, flow // 初始化layui的模板和瀑布流模块 layui.use(['laytpl', 'flow'], function () { laytpl = layui.laytpl; flow = layui.flow; }); // layui的瀑布流加载数据 function flowLoad(dateline) { flow.load({ elem: '#masonry' , isAuto: false , end: '哥,这回真的没了~' , done: function (page, next) { $.get('${base}/api/collects/' + userId + '/'+ dateline, { page: page, size: 10 }, function (res) { var lis = []; var gettpl = $('#collect-card-tpl').html(); laytpl(gettpl).render(res.data, function (html) { $(".layui-flow-more").before(html); }); next(lis.join(''), page < res.data.totalPages); }) } }); } // 点击工夫筛选,从新刷新瀑布流数据 function hendleDateline(dateline) { $('#masonry').html(''); flowLoad(dateline) } // 删除操作 function handleDel(id) { layer.confirm('是否确认删除?', function (index) { $.post('${base}/api/collect/delete?id=' + id, function (res) { if (res.code == 0) { $('#masonry-item-' + id).remove() } layer.msg(res.mess) }) layer.close(index); }); } $(function () { // 初始化加载,all示意全副 flowLoad('all') });
页面加载的时候开始执行flowload('all'),加载全副以后用户的数据。
接下来,咱们来实现内容主体局部的数据加载,在js中,咱们应用的是数据瀑布流的形式,所以在定义接口时候留神要分页哈。
@Slf4j@Controllerpublic class CollectController extends BaseController{ @Login @ResponseBody @GetMapping("/api/collects/{userId}/{dateline}") public Result userCollects (@PathVariable(name = "userId") Long userId, @PathVariable(name = "dateline") String dateline) { Page<CollectDto> page = collectService.findUserCollects(userId, dateline, getPage()); return Result.success(page); } }
除了分页信息,对应的参数还有用户Id和珍藏日期能够作为参数,查问对应用户的某个日期的珍藏列表,当日期参数为all时候查问该用户的全副,同时当查处的用户是本人的时候,能够查出公有的珍藏。
BaseController中的getPage办法,默认都是依照珍藏日期排序:
- com.markerhub.controller.BaseController#getPage
Pageable getPage() { int page = ServletRequestUtils.getIntParameter(req, "page", 1); int size = ServletRequestUtils.getIntParameter(req, "size", 10); return PageRequest.of(page - 1, size, Sort.by(Sort.Order.desc("collected"), Sort.Order.desc("created")));}
咱们来重点看看findUserCollects办法。
- com.markerhub.service.impl.CollectServiceImpl#findUserCollects
/** * 查问某用户的某个日期的珍藏 */@Overridepublic Page<CollectDto> findUserCollects(long userId, String dateline, Pageable pageable) { Page<Collect> page = collectRepository.findAll((root, query, builder) -> { Predicate predicate = builder.conjunction(); // 关联查问 Join<Collect, User> join = root.join("user", JoinType.LEFT); predicate.getExpressions().add(builder.equal(join.get("id"), userId)); // all示意查问全副 if (!dateline.equals("all")) { // 转日期格局 LocalDate localDate = LocalDate.parse(dateline, DateTimeFormatter.ofPattern("yyyy年MM月dd日")); predicate.getExpressions().add( builder.equal(root.<Date>get("collected"), localDate)); } UserDto userDto = (UserDto)httpSession.getAttribute(Const.CURRENT_USER); boolean isOwn = (userDto != null && userId == userDto.getId().longValue()); // 非自己,只能查看公开的 if (!isOwn) { predicate.getExpressions().add( builder.equal(root.get("personal"), Const.collect_opened)); } return predicate; }, pageable); // 实体转Dto return page.map(collectMapper::toDto);}
这外面,有关联查问,珍藏与用户是多对一的关系,所以绝对还是比较简单的,只须要左连贯用户表,而后让用户表的id为指定的用户ID即可。
珍藏日期这里,传进来的参数格局是这样的:yyyy年MM月dd日,所以须要转格局,让数据库能辨认比照。
而后非自己只能看公开的这里,须要从HttpSession中查看以后用户的信息,比照ID是否是同一人,非自己只能查看公开的珍藏。
最初,须要查问进去page中的内容是实体Collect的列表,须要把Collect专场CollectDto,这时候咱们又须要用到mapstruct了。
- com.markerhub.mapstruct.CollectMapper
@Mapper(componentModel = "spring", uses = {UserMapper.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)public interface CollectMapper { CollectDto toDto(Collect collect); List<CollectDto> toDto(List<Collect> collects);}
须要留神CollectDto外面有UserDto,所以咱们在@Mapper注解中须要加上UserMapper.class做对应的转化。能够看到mapstruct主动帮咱们生成的实现代码是这样的:
ok,这样咱们就实现了数据加载的接口,咱们先来看下成果:
分页成果:
这个加载更多就是layui的瀑布流数据加载给咱们生成的。
删除操作
删除操作比较简单,删除时候留神比照一下是否是以后用户的珍藏!
- com.markerhub.controller.CollectController#delCollect
@Login@ResponseBody@PostMapping("/api/collect/delete")public Result delCollect (long id) { Collect collect = collectService.findById(id); Assert.notNull(collect, "不存在该珍藏"); Assert.isTrue(getCurrentUserId() == collect.getUser().getId(), "无权限删除!"); collectService.deleteById(id); return Result.success();}
删除提醒成果:
9、新增、编辑珍藏
对于新增和编辑,业务解决差不多的,所以咱们放在同一个办法中解决。
- com.markerhub.controller.CollectController#editCollect
@Value("${server.domain}")String serverDomain;@Login@GetMapping("/collect/edit")public String editCollect(Collect collect) throws UnsupportedEncodingException { // 这段js是为了放在浏览器书签中不便前面间接珍藏某页面。 // 编码这段js: String js = "(function(){" + "var site='" + serverDomain + "/collect/edit?chatset='" + "+document.charset+'&title='+encodeURIComponent(document.title)" + "+'&url='+encodeURIComponent(document.URL);" + "var win = window.open(site, '_blank');" + "win.focus();})();"; // javascript前面的这个冒号不能编码 js = "" + URLUtil.encode(js); if (collect.getId() != null) { Collect temp = collectService.findById(collect.getId()); // 只能编辑本人的珍藏 Assert.notNull(temp, "未找到对应珍藏!"); Assert.isTrue(getCurrentUserId() == temp.getUser().getId(), "无权限操作!"); BeanUtil.copyProperties(temp, collect); } req.setAttribute("js", js); req.setAttribute("collect", collect); return "collect-edit";}
页面的话,就是一个表单:
- collect-edit.ftl
<#import "inc/layout.ftl" as Layout><@Layout.layout "珍藏操作"> <div id="app" class="row justify-content-md-center"> <div class="alert alert-info" role="alert"> 请把此链接:<a href="${js}" class="alert-link">立刻珍藏</a>,固定到浏览器的书签栏。 </div> <form class="row g-3" style="width: 500px;" id="collect-form"> <input type="hidden" name="id" value="${collect.id}"> <div class="col-12"> <label for="title" class="form-label">题目 *</label> <input type="text" name="title" class="form-control" id="title" value="${collect.title}" required> </div> <div class="col-12"> <label for="url" class="form-label">链接 *</label> <input type="text" name="url" class="form-control" id="url" value="${collect.url}" required> </div> <div class="col-12"> <label for="validationDefault04" class="form-label">笔记</label> <textarea class="form-control" name="note" id="validationDefault04">${collect.note} </div> <div class="col-12"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="personal" value="1" id="personal" <#if collect.personal == 1>checked</#if>> <label class="form-check-label" for="personal"> 公有的,不在珍藏广场中展现此珍藏! </label> </div> </div> <div class="col-12"> <button class="btn btn-primary" type="submit">提交珍藏</button> </div> </form> </div>
而后js局部:
$(function () { $("#collect-form").submit(function (event) { // 阻止提交 event.preventDefault() // 异步提交表单 $.ajax({ type: "POST", url: "/collect/save", data: $("#collect-form").serialize(), success: function(res){ layer.msg(res.mess, { time: 2000 }, function(){ location.href = "/"; }); } }); }) });</@Layout.layout>
为了保留boostrap的校验成果,而后又实现异步提交表单,所以写了js的submit()表单办法。
页面成果如下:
而后提交保留的办法:
- com.markerhub.controller.CollectController#saveCollect
@Login@ResponseBody@PostMapping("/collect/save")public Result saveCollect(Collect collect) { Assert.hasLength(collect.getTitle(), "题目不能为空"); Assert.hasLength(collect.getUrl(), "URL不能为空"); if (collect.getId() != null) { Collect temp = collectService.findById(collect.getId()); // 只能编辑本人的珍藏 Assert.notNull(temp, "未找到对应珍藏!"); Assert.isTrue(getCurrentUserId() == temp.getUser().getId(), "无权限操作!"); } User user = new User(); user.setId(getCurrentUser().getId()); collect.setUser(user); collectService.save(collect); return Result.success();}
还有service中的保留办法:
- com.markerhub.service.impl.CollectServiceImpl#save
@Override@Transactional(rollbackFor = Exception.class)public void save(Collect collect) { if (collect.getId() == null) { collect.setCreated(new Date()); collect.setCollected(new Date()); collectRepository.save(collect); } else { Collect temp = collectRepository.getById(collect.getId()); // 属性复制 temp.setTitle(collect.getTitle()); temp.setUrl(collect.getUrl()); temp.setNote(collect.getNote()); temp.setUser(collect.getUser()); temp.setPersonal(collect.getPersonal()); temp.setCollected(new Date()); collectRepository.save(temp); }}
10、珍藏广场
珍藏广场,就是所有用户公开分享珍藏的中央,只须要把所有的公开珍藏都依照分页查问进去即可。后面做过我的珍藏,其实差不多。
- com.markerhub.controller.IndexController#collectSquare
@GetMapping("/collect-square")public String collectSquare () { return "collect-square";}
因为前面我做了个搜寻性能,搜寻页面和这个珍藏广场的页面是差不多了,为了反复工作,所以这外面我退出了一下搜寻的元素。
- com.markerhub.controller.CollectController#allCollectsSquare
@ResponseBody@GetMapping("/api/collects/square")public Result allCollectsSquare() { Page<CollectDto> page = collectService.findSquareCollects(getPage()); return Result.success(page); }}
- com.markerhub.service.impl.CollectServiceImpl#findSquareCollects
@Overridepublic Page<CollectDto> findSquareCollects(Pageable pageable) { Page<Collect> page = collectRepository.findAll((root, query, builder) -> { Predicate predicate = builder.conjunction(); // 只查公开分享的 predicate.getExpressions().add( builder.equal(root.get("personal"), 0)); return predicate; }, pageable); return page.map(collectMapper::toDto);}
页面:
- collect-square.ftl
<#import 'inc/layout.ftl' as Layout><@Layout.layout "珍藏广场"> <div id="app" class="row justify-content-md-center"> <#--搜寻提醒--> <#if searchTip> <div class="alert alert-info" role="alert">${searchTip}</div> </#if> <div class="col"> <#include "/inc/collect-tpl.ftl"> <div class="row" id="masonry"></div> </div> </div> var laytpl, flow layui.use(['laytpl', 'flow'], function () { laytpl = layui.laytpl; flow = layui.flow; }); function flowLoad(keyword, userId) { flow.load({ elem: '#masonry' , isAuto: false , end: '哥,这回真的没了~' , done: function (page, next) { $.get('/api/collects/square', { page: page, size: 10, q: keyword, userId: userId }, function (res) { var lis = []; var gettpl = $('#collect-card-tpl').html(); laytpl(gettpl).render(res.data, function (html) { $(".layui-flow-more").before(html); }); next(lis.join(''), page < res.data.totalPages); }) } }); } function handleDel(id) { layer.confirm('是否确认删除?', function (index) { $.post('/api/collect/delete?id=' + id, function (res) { if (res.code == 0) { $('#masonry-item-' + id).remove() } layer.msg(res.mess) }) layer.close(index); }); } $(function () { flowLoad('${q}', ${userId}) }); </@Layout.layout>
页面成果如下:
11、搜寻性能
搜寻是个很罕用也很重要的性能,为了进步搜寻的响应速度,罕用的搜寻中间件有elasticsearch和solr,这次咱们来应用elasticsearch来配合咱们的我的项目实现搜寻性能。
那么mysql外面的数据如何与elasticsearch进行同步呢?其实解决形式还是挺多的,咱们之前在eblog我的项目中,就借助rabbitmq来配合数据同步,当后盾数据发生变化时候,咱们发送音讯到mq,生产端生产音讯而后更新elasticsearch,从而让数据达成同步。这样代码的开发量就听多了。
这次,咱们应用canal来实现数据的同步,canal是伪装成mysql的备份机基于binlog来实现数据同步的,所以在代码中,咱们就不再须要治理数据同步的问题。因而,咱们程序间接连贯elasticsearch进行搜寻性能开发就行,而后服务器上canal等中间件的装置,咱们在另外一篇文章中手把手教大家实现搭建。残缺文档:https://shimo.im/docs/TWTTkTTXGyRDcYjk
对搜寻性能的需要如下:
- 能够对某个用户进行独自搜寻
- 对搜寻广场的所有公开珍藏进行搜寻
上面咱们编写搜寻接口:
@Slf4j@Controllerpublic class SearchController extends BaseController { @GetMapping("/search") public String search (String q, Long userId) { req.setAttribute("q", q); // 独自搜寻某个用户的珍藏记录 req.setAttribute("userId", userId); String message = "正在搜寻公开【珍藏广场】的珍藏记录"; if (userId != null) { UserDto userDto = userService.getDtoById(userId); if (userDto != null) { message = "正在搜寻用户【" + userDto.getUsername() + "】的珍藏记录"; } } req.setAttribute("searchTip", message); return "collect-square"; }}
页面仍然是珍藏广场的页面。
基于我的珍藏页面搜寻的时候,搜寻的就是我本人的珍藏
基于珍藏广场页面搜寻的时候,搜寻的就是所有公开搜寻的积攒。
当然了,也能够点击某个用户的头像进去,搜寻的就是该用户的记录
公有的记录,非自己都是无奈搜寻进去的。
@Slf4j@Servicepublic class SearchServiceImpl implements SearchService { @Autowired CollectDocRepository collectDocRepository; @Autowired ElasticsearchRestTemplate elasticsearchRestTemplate; @Autowired CollectDocMapper collectDocMapper; @Autowired HttpSession httpSession; public Page<CollectDto> search(String keyword, Long userId, Pageable pageable) { Criteria criteria = new Criteria(); if (userId != null && userId > 0) { // 增加userId的条件查问 criteria.and(new Criteria("userId").is(userId)); } UserDto userDto = (UserDto)httpSession.getAttribute(Const.CURRENT_USER); if (userDto != null && userId != null) { boolean isOwn = userId.longValue() == userDto.getId().longValue(); if (isOwn) { // 如果是搜寻本人的,公开公有都能够搜寻 criteria.and(new Criteria("personal").in(0, 1)); } else { // 如果是搜寻他人的,那么只能搜寻公开的 criteria.and(new Criteria("personal").is(0)); } } else { // 未登录用户、或者搜寻广场的,只能搜寻公开 criteria.and(new Criteria("personal").is(0)); } CriteriaQuery criteriaQuery = new CriteriaQuery(criteria .and(new Criteria("title").matches(keyword)) .or(new Criteria("note").matches(keyword)) ).setPageable(pageable); SearchHits<CollectDoc> searchHits = elasticsearchRestTemplate.search(criteriaQuery, CollectDoc.class); List<CollectDoc> result = searchHits.get().map(e -> { CollectDoc element = e.getContent(); return element; }).collect(Collectors.toList()); Page<CollectDoc> docPage = new PageImpl<>(result, pageable, searchHits.getTotalHits()); log.info("共查出 {} 条记录", docPage.getTotalElements()); return docPage.map(collectDocMapper::toDto); } @Override public Page<CollectDto> searchH(String q, Pageable page) { // jpa 无奈做到条件查问,所以把userId去掉了 Page<CollectDoc> docPage = collectDocRepository.findByPersonalAndTitleLikeOrNoteLike(0, q, q, page); return docPage.map(collectDocMapper::toDto); }}
除了应用elasticsearchRestTemplate,我还借助jpa的命名标准写了一条搜寻性能searchH,然而这样的写法是在无奈做到条件搜寻,所以罗唆放弃了这种写法。
在search办法中对于是不是搜寻自己的记录做了很多判断,这里大家须要留神,其余没啥说的,都是一些条件的退出,其实elasticsearchRestTemplate还有很种搜寻写法,感兴趣的能够多多百度搜寻,不过最新版本的elasticsearch材料还是绝对比拟少的,很多老版本的写法曾经不实用了。
12、结束语
好啦,废了好长的工夫才把我的项目和所有文档写完,这还不值得你关注我的公众号:Java问答社、MarkerHub 这两个号吗,哈哈,顺便给我点个赞吧,感激,我是吕一明,此时我的原创我的项目,转载请注明出处,感激!
首发公众号:MarkerHub
作者:吕一明
原文链接:https://www.zhuawaba.com/post...
线上演示地址:https://www.zhuawaba.com/dail...
视频解说:https://www.bilibili.com/vide...
源码地址:请关注公众号:Java问答社,回复【678】获取