设计并制作一个旅游点评项目
项目参考: 马蜂窝
一、项目介绍
1. 技术栈:
数据库: mongodb + elastcearch
持久层: mongodb + redis
业务层: SpringBoot
web :SpringMVC
前端 :
管理页面: JQuery + Bootstrap3
旅游展示页面: vue + JQuery + css
2. 项目搭建:
1. 该项目主要拆分为三个模块设计:
模块一: trip-website ---> 负责前提供给用户浏览页面的展示
模块二: trip-mgrsite ---> 负责前端页面展示内容的后台管理
模块三: trip-website-api ---> 负责提供前端页面数据交互的接口
二、项目内容概要
1. 普通用户。普通用户即通过应用注册登录的用户,通过注册后可对用户进行相关权限的开放,比如写游记,点评,点赞, 收藏游记等等;
2. 后台管理员。 需要后台的管理人员或者平台运营人员来对系统中的内容进行维护。 他们工作的区域就是我们提供的应用后台管理平台。比如可以在后台发布攻略,管理用户发布的游记,审核用户的点评信息等等;
3. 游客。 没有登录的用户就是游客。 游客可以使用一部分功能,比如看看平台发布的攻略,看看别人写的游记。 但是不能做点评,写游记等登录之后才能做得;
4. 整个项目主要实现的功能为: 注册/登录、目的地的查询、 旅游攻略、 旅游日记、 内容点评、 数据统计、 首页关键字索引。
三、项目功能的实现
功能一: 注册与登录
1. 通过观察发现,用户注册到用户登录中我们需要的字段设计,如下图所示;
2. 当点击立即注册时会将填入的手机号码进行校验, 光是前端(js)的校验手机号码的合法性是不准确的, 当前端校验完毕后会通过异步请求的方式把手机号码作为参数提交到后台, 通过api模块编写一个checkPhone的方法,通过用户的服务接口,在使用JPA的规范进行对mongodb数据库的查询(通过手机号码查询用户信息)之前,需要对手机号码的(格式,长度, 为空...)进行校验,最后通过查询数据库判断手机号码的用户是否存在。
3.如果不存在该用户,表示手机号码没有被注册过,点击进入到用户注册页面,短信验证的处理:
1.通过uuid提取前四位创建短信的验证码
2.通过StringBuilder拼接一个短信验证信息
3.通过springmvcc提供的htttp请求的 操作类RestTemplate,调用第三方的短信接口实现短信的发送
4.因为短信验证有过期时间, 短信发送成功后将验证码保存到redis中,在用户的缓存服务接口中设计一个setVerifyCode方法
5. redis的key的设计, 需要准许可读性, 唯一性, 灵活性等要素设计, 这里设计为"verify_code:" + phone作为key, 验证码作为value值,此外这里有用到时间, 时间一般为固定常量, 可以在core中编写一个Constsutil设计时间单位
6.点击注册:(充分的号码校验是为了保证代码的健壮性)
1> 参数是否为空校验
2> 手机格式是否正确(正则)
3> 手机号码是否注册过(查表)
4> 两次输入的密码是否一致(比较)
5> 短信验证是否正确(比较)
1> 获取手机号码与验证码
2> 将手机号码拼接成key, 通过key去redis数据库中进行查找
3> 验证码存在并正确则通过, 否则失败
6.因为mongodb是全量保存, 为了保证数据库中的其他字段默认值不为null, 这里需要手动设置
7.为了保证美观, 错误的参数都以抛出异常方式处理,可以设计一个断言类进行异常信息的自定义
8. 设计一个自定义异常类, 这里业务的异常使用LoginException, 非业务范围使用Exception, 及业务是给用户看的(手机号码错误, 密码不一致等),非业务是自己代码引起的问题,是不给用户看的
PS: 这里为什么要使用redis, 用mongodb或session不行吗?
1> redis可以满足多个请求间的数据共享
2> redis的定义就是一个临时存储的数据库,对于需要进行数据失效很方便
3> 因为涉及存储的数据较多,所以不建议使用session
4> mogodb, mysql也可以做到, 不过对有效时间的控制需要另外处理
功能二: 登录
需求: 手机号码必填,登录失败的提示, 登录成功跳转到首页
1. 登录逻辑分析
1> 除了通过前端的对手机号码和密码的格式进行校验外, 后端也需要保证保证代码的健壮性再次进行校验
2> 获取username(手机号码)和password ,查询mongodb数据库, 对象若存在获取该对象,登录成功, 否则登录失败
3> 当登录成功后, 为了提升用户的使用体验,即在用户点击其他页面的时候依旧处于登录状态,这里需要使用token令牌的方式,对用户信息做一个缓存
4> 为了保证redis中key的唯一性, redis的
key存储uuid生产的token值,
value存储登录用户对象,
5> 给这个token设置超时时间, 30分钟
6> 把这个token随机码返回给客户端
7> 客户端拿到给token令牌后,需要对给令牌进行保证, 每次发送请求的时候需要把token发在请求头中发送到后端
8> 处理客户端请求的时候需要使用requers.getHead(“token”)对请求头中的token进行获取, 并通过该token去redis中查询, 获取该用户对象
1. 每次在redis中查询到存在该key则需要对token的存在时间进行重新设定, 30分钟
9> 最后根据用户对象是否存在进行判断是否登录
10> 这里我使用interceptor拦截器对访问的页面进行拦截, 相对于filter过滤器, interceptor为springmvc的组件,功能复杂, 操作简单
11> 在api的主方法中,通过注入bean的方式对所以访问进行拦截, 放行(登录, 注册, 验证码等请求)
PS: 这里因为使用了不同的端口访问,涉及到http跨域同源的问题,解决:这里需要在api的主方法中,提供一个跨域访问的corsConfigurer()方法,对GET, POST等等方法进行放行.
功能三: 热门目的地
需求: 热门目的地主要分为两个板块实现: 一个是热门的区域, 一个是热门区域下面所挂载的目的地 .
1. 热门区域的分析
1> 当进入到目的地的页面时,会发起热门区域的查询,即对mongodb中destination_region表中状态为热门的区域进行查询并把区域对象返回
2> 国内是固定写死的,中国第一!
2. 热门目的地的分析
1> 当鼠标移动到热门的区域,同时下方立即显示当前区域对应的市,区等
2> 获取区域id即refionId,内地为-1, 其他为正常默认的区域id
3> 创建一个泛型为Destination对象的list集合,目的是存储所以的目的地对象
4> 根据获取的区域id, 如果是-1 ---> 查询destination目的地表,所有ParentName字段为中国的集合
5> 否则,先根据区域regionId查询destination_region表中的区域对象
6> 通过区域id查询出来的区域对象,通过getRefIds()方法获取该对象中的refId字段的所以直辖市的集合
7> 直辖市list集合通过jpa中In语法查询destination目的地表,获取直辖市下面的所有目的地,放到list集合中
8> 只查询前五个,可以遍历该集合, 使用PageRequest.of(0, 5)添加条件,在把前五个数据放到Destination类中写好的子地区list字段中, 最后返回list
3. 目的地的明细分析
1. 吐司
需求: 当从热门目的地页面点击一个目的地进来的时候, 跳转到目的地明细的页面, 页面上方有导航吐司的显示
1> 获取页面的目的地id, 通过该目的地id查询所以的父级目的地
2> 对传进来的id进行空值判断,如果为空则返回Collections.emptyList()一个空值的集合
3> 创建一个泛型为Destination对象的list集合,目的是存储所以的目的地对象
4> 查询当前目的地的父亲, 使用一个递归的调用, 查询条件满足当前目的地的父类id存在则调用自己进行添加到集合中,否则return
5> 通过Collections.reverse(list), 调换集合顺序
6> 返回一个list集合
2. 攻略概况 / 攻略概况明细
需求: 显示当前目的地所对应的所以攻略分类,同时关联查询各个分类下的所以攻略
1> 获取页面的目的地id, 通过该目的地id查询攻略目录表获得所有的攻略分类
2> 遍历获取到的所有攻略分类List
3> 返回list
3. 攻略点击量前3的文章显示
1> 在qo中设置分页目的地id,设置qo的分页条件
2> 在query查询方法中添加目的地id和文章id的条件判断
3> 使用DBHelper方法返回pageable对象
4. 查看攻略详细情况
5. 查看所有
3. 目的地的明细分析
1:攻略评论
1>评论对象设计
用户id 用户名 用户头像 用户等级 发表时间 攻略id 攻略标题 评论内容 点赞数
2>评论逻辑实现
1) 判断当前是否登录, 使用@UserInfo注解
2) 通过@UserInfo注解获取当前的登录用户信息
3) 使用BeanUtil.copyProperties(原用户对象, 新用户对象)把当前登录的用户信息设置到StrategyComment表中
4) 评论信息的设置 : 通过strategyCommentService的服务接口实现
设置评论的更新时间
设置主键id为null-->避免出错
5)更新完数据后进行保存,通过评论的持久层接口 StrategyCommentRepository.save(comment)进行保存
3>评论分页查询
1)使用strategyCommentService.query(qo)进行条件的设置,使用DBHelper.query的工具类, 这里默认根据strategyId分页
2)前端需要map.page, 使用newParamMap().put("page", page)进行返回
2:评论点赞
1>评论点赞分析理解透
1) 在旅游评论类中设计一个点赞数的list集合,当当前的旅游评论被当前用户点赞后
2) 判断当前登录的用户id是否存在集合中,
不存在则查询当前的旅游点评表thumbupnum字段+1,并把用户id设置到list集合中
存在则查询当前的旅游点评表thumbupnum字段+1, 并把当前用户移除list集合
2>评论点赞实现
1) 判断当前是否登录, 使用@UserInfo注解
2) 前端提交了两个参数, 一个cid: 当前攻略评论id,sid: 用户id
3) 通过comment的服务接口, 传入cid和sid
4) 判断是点赞还是取消点赞
通过cid查询当前的攻略评论对象
获取该对象的list点赞集合
通过list.contains(sid)判断集合中是否存在sid
5) 不存在: 则进行点赞
comment对象设置thumbupnum字段+1并把用户添加到集合中
6) 存在: 则取消点赞
comment对象设置thumbupnum字段-1
在list集合中移除当前用户id
7)保存StrategyCommentRepository.save(comment)
8)设置分页
1> 使用strategyCommentService.query(qo)进行条件的设置,使用
DBHelper.query的工具类, 这里默认根据strategyId分页
2> 前端需要map.page, 使用new ParamMap().put("page",page)进行返回
9)返回一个page
3:游记评论
1>评论对象设计
用户id 用户名字 用户等级 用户头像 游记id 游记标题 游记评论内容
最新的修改时间 评论类别 关联的评论 关联内容
2>评论逻辑实现
1)判断当前是否登录, 使用@UserInfo注解
2)通过@UserInfo注解获取当前的登录用户信息
3)使用BeanUtil.copyProperties(原用户对象, 新用户对象)
把当前登录的用户信息设置到StrategyComment表中
4)评论信息的设置 : 通过strategyCommentService的服务接口实现
设置评论的更新时间
设置主键id为null-->避免出错
5)判断评论的评论的是否存在
存在则把该评论的评论设置到Travelcomment中
1.通过传进来的游记对象获取评论的评论的对象字段
2.获取评论的评论的对象的id,进行判断
3.存在: 把该评论的评论对象设置到当前的游记对象中
6)更新完数据后进行保存,通过评论的持久层接口
StrategyCommentRepository.save(comment)进行保存
3>评论分页查询
1) 使用strategyCommentService.query(qo)进行条件的设置,使用
DBHelper.query的工具类, 这里默认根据strategyId分页
2) 前端需要map.page, 使用newParamMap().put("page", page)进行返回
4:数据统计
1>统计对象理解
1) 使用redis缓存来减轻mongodb的压力, 即减少MQL的操作
2) 把阅读数, 评论数, 收藏数, 点赞数用一个vo对象封装,
这样在给页面响应数据的时候更方便
VO对象的设计:
private String strategyId; //攻略id
private int viewnum; //点击数
private int replynum; //攻略评论数
private int favornum; //收藏数
private int sharenum; //分享数
private int thumbsupnum; //点赞个数
2>阅读数操作
1)通过strategyStatisVoService服务创建一个阅读数添加的方法,
传入参数为(攻略id, 每次访问+1)
2)通过Rediskeys.STRATEGY_STATIS_VO.join(sid), 设置好redis中的key值
即 strategy_statis_vo : sid(键) vo对象(值)
3)首先需要判断VO对象是否存在
通过StringRedisTemplate服务.hasKey(key)进行判断
4)key不存在: 就把mongodb数据库中的阅读数取出来, 即Strategy strategy =strategyService.get(sid);
然后创建一个VO对象, 使用BeanUtils.copyProperties(strategy, vo);进行赋值
注意: strategy中没有sid, 需要自己手动加入, vo.setStrategyId(sid);
把key和VO对象存到redis中, 注意redis的值是json格式,需要
template.opsForValue().set(key,JSON.toJSONString(vo));
5)key存在: 直接通过key获取对应的vo对象, 并把vo对象转为json格式
StringvoStr = template.opsForValue().get(key);
vo= JSON.parseObject(voStr, StrategyStatisVO.class);
6)进行阅读数+1操作
VO对象vo.setViewnum(vo.getViewnum() + i);
7) 把数据重新更新到redis中
重新获取vo的键, 然后设置键和值
Stringkey = Rediskeys.STRATEGY_STATIS_VO.join(vo.getStrategyId());
template.opsForValue().set(key,JSON.toJSONString(vo));
PS:这里要暴露一个接口出去, 控制类才能将修改/新的vo的阅读数据查询并返回
3>回复数操作
如上,
也是需要判断vo对象是否存在
其次对回复数进行+1操作
最后更新到redis数据库中
4>收藏数操作
攻略的收藏redis设置: key: user:strategy:uid value: sid
表示: 当前用户 是否收藏有当前的攻略
1)生成攻略收藏的key, 并判断该key是否在redis中
key存在: 获取ke所对应的值(是一个集合攻略id), 并把该值转换为list集合
key不存在: 创建一个新的list集合
2)区分收藏与否:
判断list集合中是否存在当前攻略id
存在: 取消收藏, vo对象的收藏统计-1, 并把sid从list中移除
不存在: 收藏 vo对象的收藏统计+1, 并把sid从list中添加
3)保存于更新: 把key 和 value保存到redis中
更新vo对象
1:点赞逻辑-体会redis中key的灵活性
1>分析
需求: 必须登录之后才可以进行 ---> 点赞
--->判断今天是否第一次点赞
--->没有:点赞成功--->有: 点赞失败
注意: 这里使用标记进行是否点赞的判断
2>实现
传入sid和uid
1) 创建好以角色为角度的一个key: 即 strategy_uid_sid: 表示该用户的攻略有哪些,
value 是 用户所有的攻略id
2) 获取创建好的key去redis中查
不存在:
获取vo对象的themnsupnum+1, 并设置到vo对象中
设置这个key的超时时间: (今晚最后一秒 - 当前的时间 ),使用DateUtil工具类
并把key, 标记, 超时时间设置到redis中
存在:
已经点赞了, 直接返回false(前端需要boolean)
2:redis数据初始化
1>分析
1)每次都会判断这个东西是否存在, 是不是每次都会进行数据初始化处理
2)库里面的数据和缓存中的数据是否同步
2>spring事件监听
1)选着事件监听, 可以再spring启动之后立刻进行数据的初始化, 提升用户体验,
3>初始化
初始化逻辑特点: 一次性初始即可,容器启动成功把所有数据都准备好给你
1) 监听器的包: 初始化把vo的数据放到redis中
2) 实现一个监听接口 ApplicationListener
容器创建完成(aop ioc di)之后 就执行改方法
3) 贴上一个@Component交给spring管理
4) 实现这个接口的方法,可以再里面数据的初始化,
把所有攻略数据查询出来放到redis中
注意: 如果vo对象已经存在redis中了, 不需要再次初始化!!, 否则会导致前端提交的数据丢失
解决: 在把数据存到redis中的时候,判断redis中是否存在该key
存在则使用continue跳过
3:redis数据持久化
1>分析
1) 为什么进行持久化: 缓存数据发生变动, 如果不进行持久化数据可能会丢失, 所以需要将数据持久化到数据库
2) 需要持久化的数据是什么: 缓存中有用的数据, 此处需要持久化的数据: (1):VO数据 (2) 用户攻略收藏数据 `[扩展]`
3)在哪一个项目执行持久化逻辑(mgrsite web-api)
4)在javaweb那个组件中实现持久化逻辑(filter servlet controlelr interceptot listener)
持久化逻辑特点: 多次执行, 人工执行请求进行持久化, 程序周期性执行
持久化最佳实践是: 使用定时器, 周期执行, 周期性执行(每个1天执行一次)
2>spring定时
1) 创建一个定时类记得贴注解:交给spring,
该类中的方法@Scheduled: 定时任务标签, cron=""定时任务计划
2) 在main方法中贴上@EnableScheduling 启动定时任务
3) Seconds Minutes Hours DayofMonth Month DayofWeek (springboot支持的格式)
秒 分 小时 天 月 周
0 0 2 1 * ? * 表示每月的1日的凌晨2点执行
3>持久化
就是把redis中的数据存放到mongodb数据库中, 保证数据不会丢失
数据的持久化需要和定时器类一起使用, 表示在某一个时间内自动保存数据到mongoddb
1) 需要获取redis中的vo数据
2) 创建通配表达式来获取所有的vo对象: strategy_startid_vo: *
3)传入统配表达式, 返回vo对象集合
4) 通过redis的,keys(通配表达式)获取所有的vo对象的key集合
5) 遍历该集合, 将从redis中获取vo对象
6)获取到一个就使用JSON.parseObject解析该value
7) 把解析好的value存放到新建的泛型是Vo对象的集合
8)在controller中, 把获取的vo对象进行遍历
9) 根据id的条件进行mongodb数据的更新
注意: 这里使用了DBhelp工具, 里面穿的是字节码对象,
所以需要把vo使用BeanUtil.Copy进行对象的赋值, 再传入