骡窝窝项目总结
一、骡窝窝项目概要
技术路线
1,数据库:mongodb + elasticsearch;
2,持久化层:mongodb+Redis (缓存);
3,业务层:Springboot;
4,Web:SpringMVC;
5,前端:
管理后台:jQuery+Bootstrap3
前端展示:vue +jquery + css;
项目拆分
拆分便于项目的维护与扩展,更方便于多服务的部署。
构建多模块项目
![](https://img.haomeiwen.com/i11635091/780a8fe6861c6f89.png)
![](https://img.haomeiwen.com/i11635091/a1b40e9e50ea275a.png)
parent项目如何对依赖做管理:
-
1 :如果所有子项目都需要依赖某个jar包,将
这个jar配置parent项目的<dependances>
所有子项目可以共享 -
2 :如果某些子项目需要依赖某个jar ,某些子
不需要依赖jar ,将这个jar配置parent项目的
< ManagementDependances>配置配置jar
不会再所有子项目共享, parent仅仅是管理这,个依赖版本,需要引用jar的子项目需要单独引入jar包,但是可以不写版本信息。 -
3.如果就某一个项目需要依赖某个jar,该子项目自己引入jar即可。
二、 用户注册
![](https://img.haomeiwen.com/i11635091/58885c68b4e63ea0.png)
1、手机号码格式
使用正则表达式验证
2、手机号的唯一验证
查询用户表的手机号字段是否存在数据库中
3、注册参数校验
校验参数是否为空值
4、短信验证码
验证码发送分析.png
-
页面倒计时
-
使用短信网关api接口(京东万象)
短信发送分析.png
令牌登录分析.png
-
短信验证码存入redis,设置失效时间
verify_code:13700001111
-- 设置有效性5分钟
三、 redis使用与设计
(一)、简介
key-value型的非关系型数据库,其实是一个缓存,数据可能会丢失
https://blog.csdn.net/aaronthon/article/details/81714528
问题:什么情况用缓存?为什么要用缓存
当某个操作需要频繁操作(读与写)数据库,使用缓存可以减少对数据库操作压力
提升系统性能。
Memcached与Redis有什么区别
https://www.cnblogs.com/middleware/articles/9052394.html
优点:
相比mysql性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
将io的操作变为在内存操作(缓存)
单线程(6版本之后是多线程)
缺点
无事务处理
redis定位是缓存,缓存有可能会丢失
Redis 事务的执行并不是原子性,本质是一个批量执行
实现缓存
-
1:map
优点:jdk自带类,操作简单,功能相对简单
缺点:无法保存,数据容易丢失 -
2: ehcache
缺点:支持单体项目,对分布式项目或集群项目支持不好,扩展性相对弱一点 -
3:redis - memcache
redis是一个独立的缓存框架。不受项目的结构的限制。对多架构项目支持非常友好,扩展也方便
相比memcache支持的数据类型更多,memcache只支持string类型
(二)、配置
redis.windows.conf
配置文件
- 远程联接
protected-mode:设置为no,保护模式关闭,可以远程访问
bind : 绑定填写远程连接的服务器IP地址,默认为127.0.0.1,0.0.0.0 表示所有服务器都可以访问本机 - 密码
# requirepass foobared
去掉#号使用密码
(三)、redis命令
clear清除屏幕
String 字符串命令
![](https://img.haomeiwen.com/i11635091/92c12c183ba18605.png)
hash命令
java中使用spring封装的redis对象StringRedisTemplate来操作时,Mp<string, Map<string, ?>> 重复会覆盖,但hashvalue的值会累加
![](https://img.haomeiwen.com/i11635091/5b710d214b161f45.png)
list命令
![](https://img.haomeiwen.com/i11635091/87ff8a742b894824.png)
(四)、Redis内存淘汰机制及过期Key处理
(五)、 java 操作redis
Jedis基本使用
public class JedisTest {
// 1:创建Jedis连接池
JedisPool pool = new JedisPool("localhost", 6379);
// 2:从连接池中获取Jedis对象
Jedis jedis = pool.getResource();
/* 设置密码jedis.auth(密码); */
// jedis将redis中命令封装方法,名字都不改
public static void close(Jedis jedis, JedisPool pool) {
// 4:关闭资源
jedis.close();
pool.destroy();
}
@Test
public void testJedisPoolString() {
jedis.set("age", "2");
jedis.set("sex", "女");
System.out.println("根据键取出值:" + jedis.get("age"));
System.out.println("把值递增1:" + jedis.incr("age"));
System.out.println("把值递减1:" + jedis.decr("age"));
System.out.println("偏移值+2:" + jedis.incrBy("age", 2));
System.out.println("设置失效时间:" + jedis.expire("age", 10));
System.out.println("查询key过期时间:" + jedis.ttl("age"));
System.out.println("批量查询键值:" + jedis.mget("age","sex"));
System.out.println("批量查询键值:(新键值长度为)" + jedis.append("sex","性"));
System.out.println("根据键取出值:" + jedis.get("sex"));
System.out.println("存入键值对,键存在时不存入:" + jedis.setex("age",10,"键同名不存入"));
System.out.println("批量查询键值:" + jedis.mget("age","sex"));
System.out.println("修改键对应的值(长度为):" + jedis.setrange("age",0,"changeValue"));
System.out.println("根据键取出值:" + jedis.get("age"));
System.out.println("根据键删除键值对:" + jedis.del("age"));
System.out.println("根据键删除键值对:" + jedis.del("sex"));
close(jedis, pool);
}
@Test
public void testJedisPoolHash() {
jedis.hset("study", "english", "1");
jedis.hset("study", "math", "2");
jedis.hset("study", "history", "3");
System.out.println("根据hash对象键取去值:" + jedis.hget("study", "math"));
System.out.println("获取该key所有hash对象:" + jedis.hkeys("study"));
System.out.println("判断hash对象是含有某个键:" + jedis.hexists("study", "math"));
System.out.println("根据hashkey删除hash对象键值对:" + jedis.hdel("study", "math"));
System.out.println("修改hash对象值:" + jedis.hset("study", "history", "6"));
System.out.println("获取hsah对象值:" + jedis.hget("study","history"));
System.out.println("根据键删除键值对:" + jedis.del("study"));
close(jedis, pool);
}
@Test
public void testJedisPoolList() {
jedis.rpush("name", "liChina", "max", "Laura");
System.out.println("范围显示列表数据,全显示则设置0 -1 :" + jedis.lrange("name", 0, -1));
System.out.println("弹出列表最左边的数据:" + jedis.lpop("name"));
System.out.println("往列表左边添加数据:" + jedis.lpush("name", "left"));
System.out.println("弹出列表最右边的数据:" + jedis.rpop("name"));
System.out.println("往列表右边添加数据:" + jedis.rpush("name", "right"));
System.out.println("范围显示列表数据:" + jedis.lrange("name", 0, -1));
System.out.println("根据索引修改元素值:" + jedis.lset("name", 1,"center"));
System.out.println("范围显示列表数据:" + jedis.lrange("name", 0, -1));
System.out.println("获取列表长度:" + jedis.llen("name"));
System.out.println("根据键删除键值对:" + jedis.del("name"));
close(jedis, pool);
}
@Test
public void testJedisPoolSet() {
jedis.sadd("hobby1", "java", "php", "c");
jedis.sadd("hobby2", "c++", "python", "c");
System.out.println("列出set集合中的元素:" + jedis.smembers("hobby1"));
System.out.println("列出set集合中的元素:" + jedis.smembers("hobby2"));
System.out.println("随机弹出集合中的元素:" + jedis.spop("hobby1"));
System.out.println("返回hobby1中特有元素(差集):" + jedis.sdiff("hobby1", "hobby2"));
System.out.println("返回两个set集合的交集:" + jedis.sinter("hobby1", "hobby2"));
System.out.println("返回两个set集合的并集:" + jedis.sunion("hobby1", "hobby2"));
System.out.println("随机获取set集合中元素:" + jedis.srandmember("hobby1"));
System.out.println("删除set集合中的元素:" + jedis.srem("hobby1", "c"));
System.out.println("列出set集合中的元素:" + jedis.smembers("hobby1"));
System.out.println("根据键删除键值对:" + jedis.del("hobby1"));
System.out.println("根据键删除键值对:" + jedis.del("hobby2"));
close(jedis, pool);
}
@Test
public void testJedisPoolZset() {
jedis.zadd("myzset", 9, "num1");
jedis.zadd("myzset", 10, "num2");
jedis.zadd("myzset", 12, "num3");
System.out.println("zset按照分数升序:" + jedis.zrange("myzset", 0, -1));
System.out.println("zset按照分数降序:" + jedis.zrevrange("myzset", 0, -1));
System.out.println("zset元素个数:" + jedis.zcard("myzset"));
System.out.println("zset元素偏移num2对应的分数-4:" + jedis.zincrby("myzset", -4, "num2"));
System.out.println("zset修改元素值:" + jedis.zadd("myzset", 10, "num1"));
System.out.println("zset返回指定成员的分数值:" + jedis.zscore("myzset", "num1"));
System.out.println("zset升序返回num3排名:" + jedis.zrank("myzset", "num3"));
System.out.println("zset降序返回num3排名:" + jedis.zrevrank("myzset", "num3"));
System.out.println("根据键删除键值对:" + jedis.del("myzset"));
close(jedis, pool);
}
public static void main(String[] args) {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
////最大连接数, 默认8个
config.setMaxTotal(100);
////最大空闲连接数, 默认8个
config.setMaxIdle(20);
//获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1
config.setMaxWaitMillis(-1);
//在获取连接的时候检查有效性, 默认false
config.setTestOnBorrow(true);
JedisPool pool = new JedisPool(config, "127.0.0.1", 6379, 5000);
Jedis j = pool.getResource();
j.set("name", "li");
String name = j.get("name");
System.out.println(name);
j.close();
pool.close();
pool.destroy();
}
}
集成SpringBoot RedisTemplate
@SpringBootTest
public class SpringRedisTest {
// 约定:所有redis操作, key value 都是字符串
@Autowired
private StringRedisTemplate redisTemplate;
@Test
public void testRedisTemplateString() {
System.err.println("========== string命令");
redisTemplate.opsForValue().set("age","2" );
System.out.println("根据键取出值:" + redisTemplate.opsForValue().get("age"));
System.out.println("把值递增1:" + redisTemplate.opsForValue().increment("age"));
System.out.println("把值递减1:" + redisTemplate.opsForValue().decrement("age"));
System.out.println("偏移值+2:" + redisTemplate.opsForValue().increment("age",2));
// 存入键值对,timeout表示失效时间,单位s
System.out.println("设置失效时间:" + redisTemplate.expire("age", 10, TimeUnit.SECONDS));
System.out.println("查询key过期时间:" + redisTemplate.opsForValue().getOperations().getExpire("age"));
// 修改
redisTemplate.opsForValue().set("age","3",0);
System.out.println("根据键取出值:" + redisTemplate.opsForValue().get("age"));
System.out.println("根据键删除键值对:" + redisTemplate.delete("age"));
}
@Test
public void testRedisTemplateList() {
System.err.println("========== list命令");
redisTemplate.opsForList().rightPush("mylist", "apple");
redisTemplate.opsForList().rightPush( "mylist", "banana");
redisTemplate.opsForList().leftPush("mylist", "pear");
System.out.println("范围显示列表数据,全显示则设置0 -1 :" + redisTemplate.opsForList().range("mylist",0,-1));
System.out.println("弹出列表最左边的数据:" + redisTemplate.opsForList().leftPop("mylist"));
System.out.println("往列表左边添加数据:" + redisTemplate.opsForList().leftPush("mylist","left"));
System.out.println("弹出列表最右边的数据:" + redisTemplate.opsForList().rightPop("mylist"));
System.out.println("往列表右边添加数据:" + redisTemplate.opsForList().rightPush("mylist","right"));
System.out.println("范围显示列表数据:" + redisTemplate.opsForList().range("mylist",0,-1));
System.out.println("获取列表长度:" + redisTemplate.opsForList().size("mylist"));
System.out.println("根据键删除键值对:" + redisTemplate.delete("mylist"));
}
@Test
public void testRedisTemplateHash() {
System.err.println("========== hash命令");
redisTemplate.opsForHash().increment("study", "english",1);
redisTemplate.opsForHash().increment("study", "math",2);
redisTemplate.opsForHash().increment("study", "history",3);
System.out.println("获取key所有hashkey:" + redisTemplate.opsForHash().keys("study"));
System.out.println("根据hash对象键取值:" + redisTemplate.opsForHash().get("study", "math"));
System.out.println("判断hash对象是含有某个键:" + redisTemplate.opsForHash().hasKey("study", "math"));
System.out.println("根据hashkey删除hash对象键值对:" + redisTemplate.opsForHash().delete("study","math"));
redisTemplate.opsForHash().increment("study", "english",6);
System.out.println("根据键删除键值对:" + redisTemplate.delete("study"));
}
@Test
public void testRedisTemplateZset() {
System.err.println("========== zset命令");
redisTemplate.opsForZSet().add("myzset", "num1", 9);
redisTemplate.opsForZSet().add("myzset", "num2", 10);
redisTemplate.opsForZSet().add("myzset", "num3", 12);
System.out.println("zset按照分数升序:" + redisTemplate.opsForZSet().range("myzset", 0, -1));
System.out.println("zset按照分数降序:" + redisTemplate.opsForZSet().reverseRange("myzset", 0, -1));
System.out.println("zset元素个数:" + redisTemplate.opsForZSet().zCard("myzset"));
System.out.println("zset元素偏移num2对应的分数-4:" + redisTemplate.opsForZSet().incrementScore("myzset", "num2", -4));
System.out.println("zset升序返回num3排名:" + redisTemplate.opsForZSet().rank("myzset", "num3"));
System.out.println("zset降序返回num3排名:" + redisTemplate.opsForZSet().reverseRank("myzset", "num3"));
// 修改值
redisTemplate.opsForZSet().add("myzset", "num1", 16);
System.out.println("zset返回指定成员:" + redisTemplate.opsForZSet().score("myzset", "num1"));
System.out.println("根据键删除键值对:" + redisTemplate.delete("myzset"));
}
@Test
public void testRedisTemplateSet() {
System.err.println("========== set命令");
redisTemplate.opsForSet().add("hobby1", "java", "php", "c");
redisTemplate.opsForSet().add("hobby2", "c++", "python", "c");
System.out.println("列出set集合中的元素:" + redisTemplate.opsForSet().members("hobby1"));
System.out.println("列出set集合中的元素:" + redisTemplate.opsForSet().members("hobby2"));
System.out.println("随机弹出集合中的元素:" + redisTemplate.opsForSet().pop("hobby1"));
System.out.println("返回hobby1中特有元素(差集):" + redisTemplate.opsForSet().difference("hobby1","hobby2"));
System.out.println("返回两个set集合的交集:" + redisTemplate.opsForSet().intersect("hobby1","hobby2"));
System.out.println("返回两个set集合的并集:" + redisTemplate.opsForSet().union("hobby1","hobby2"));
System.out.println("随机获取set集合中元素:" + redisTemplate.opsForSet().randomMember ("hobby1"));
System.out.println("删除set集合中的元素:" + redisTemplate.opsForSet().remove("hobby1","c"));
System.out.println("列出set集合中的元素:" + redisTemplate.opsForSet().members("hobby1"));
System.out.println("根据键删除键值对:" + redisTemplate.delete("hobby1"));
System.out.println("根据键删除键值对:" + redisTemplate.delete("hobby2"));
}
}
(六)、总结
1: 项目操作涉及到缓存操作, 首选 redis
2: 如果确定使用redis, 此时需要考虑使用哪个数据类型
1>如果要排序选用zset
2>如果数据是多个且允许重复选用list
3>如果数据是多个且不允许重复选用set
4>剩下的使用string
3:怎么设计 key 与 value值
https://blog.csdn.net/ahilll/article/details/84564153
4:redis持久化机制
https://www.cnblogs.com/tdws/p/5754706.html
-
RDB方式
在RDB方式下,你有两种选择,一种是手动执行持久化数据命令来让redis进行一次数据快照,另一种则是根据你所配置的配置文件 的 策略,达到策略的某些条件时来自动持久化数据。而手动执行持久化命令,你依然有两种选择,那就是save命令和bgsave命令。 -
AOF快照方式
redis.windows.conf配置文件中的appendonly修改为yes。开启AOF持久化后,你所执行的每一条指令,都会被记录到appendonly.aof文件中。但事实上,并不会立即将命令写入到硬盘文件中,而是写入到硬盘缓存,在接下来的策略中,配置多久来从硬盘缓存写入到硬盘文件。所以在一定程度一定条件下,还是会有数据丢失,不过你可以大大减少数据损失。 -
区别
RDB每次进行快照方式会重新记录整个数据集的所有信息。RDB在恢复数据时更快,可以最大化redis性能,子进程对父进程无任何性能影响。
AOF有序的记录了redis的命令操作。意外情况下数据丢失甚少。他不断地对aof文件添加操作日志记录
四、用户登录
跨域访问
解决跨域访问问题
跨域请求原理:
- 浏览器如果发起异步请求,发现请求路径url是跨域请求,浏览器会使用POTIONS方式发起总攻url请求
,携带是否许可访问请求数据 - 服务器接收这个请求,进行对比根据之前的跨域配置判断是否允许这个ip端口访问呢,如果允许响应允许访问信息,如果不允许,响应会不允许
- 浏览器接收这个服务器反馈,根据反馈的结果来决定是否发起真正请求
如果允许跨域,以真正请求方式发起,如果不允许跨域,返回时不允许跨域的异常
步骤1:implements WebMvcConfigurer
步骤2:
//跨域访问
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
//重写父类提供的跨域请求处理的接口
public void addCorsMappings(CorsRegistry registry) {
//添加映射路径
registry.addMapping("/**")
//放行哪些原始域
.allowedOrigins("*")
//是否发送Cookie信息
.allowCredentials(true)
//放行哪些原始域(请求方式)
.allowedMethods("GET", "POST", "PUT", "DELETE","OPTIONS")
//放行哪些原始域(头部信息)
.allowedHeaders("*")
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
.exposedHeaders("Header1", "Header2");
}
};
}
token令牌方式登录流程
![](https://img.haomeiwen.com/i11635091/c7180b945732c0eb.png)
登录控制
在配置类里配置CheckLoginInterceptor拦截器
定义:CheckLoginInterceptor
配置:CheckLoginInterceptor
在主配置类中implements WebMvcConfigurer
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkLoginInterceptor())
.addPathPatterns("/**");
}
使用自定义注解@RequireLogin的方式区分需要拦截登录的控制器
当前用户注入
- 自定义参数解析器
- 添加自定义参数解析器
使用addArgumentResolvers 在启动类里注册该参数解析器
五、 目的地
后端
区域管理
- 列表
PageRequest进行分页操作
@Override
public Page<Region> query(QueryObject qo) {
//1: 创建查询条件
Query query = new Query();
//2: 每页显示条数集合: list
//设置页面显示条数, 还有当前页
PageRequest pageable = PageRequest.of(qo.getCurrentPage() - 1, qo.getPageSize(),
Sort.Direction.DESC, "_id");
//3:调用dbhelper类
return DBHelper.query(template, Region.class, query, pageable);
}
- 添加
带搜索框下拉框bootstrap-select - 编辑
回显
$('#refIds').selectpicker('val', refIds);
$('#refIds').selectpicker('refresh');
- 查看
- 删除
- 热门
改变区域表里ishot的字段(0/1)
目的地管理
目的地的crud
吐司
@Override
public List<Destination> getToasts(String parentId) {
// 中国 》 广东 》 广州
if (!StringUtils.hasLength(parentId)) {
return Collections.emptyList();
}
List<Destination> list = new ArrayList<>();
createToast(list, parentId);
Collections.reverse(list); // 集合反转
return list;
}
private void createToast(List<Destination> list, String parentId) {
// 广州
Destination dest = this.get(parentId);
list.add(dest);
// 有父节点则调用自身
if (StringUtils.hasLength(dest.getParentId())) {
createToast(list, dest.getParentId());
}
}
前端
- 前端目的地的切换 -热门的目的地
鼠标移动到不同的区域名称上,异步请求查询出该区域下的目的地,分三级区域、国家、城市
@Override
public List<Destination> queryByRegionIdForApi(String regionId) {
List<Destination> list;
// 区分是否国内,查询省份
if ("-1".equals(regionId)) {
// 查询所有省份
list = repository.findByParentName("中国");
} else {
// 非国内
Region region = regionService.get(regionId);
List<String> ids = region.getRefIds();
list = repository.findByIdIn(ids);
}
// 查询第二层 :找儿子
for (Destination dest : list) {
// 显示前5个: 知识点:jpa方法怎么分页显示i
PageRequest pageRequest = PageRequest.of(0, 5, Sort.Direction.ASC, "_id");
List<Destination> children = repository.findByParentId(dest.getId(), pageRequest);
dest.setChildren(children);
}
return list;
}
六、 vue的使用
(一)、vue的使用
直接用 <script>
引入
开发版本:https://cn.vuejs.org/js/vue.js
(二)、生命周期图示
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
![](https://img.haomeiwen.com/i11635091/9e88a7b2481182bd.png)
(三)、vue常见指令
- {{}}
vue一直解析数据的指令 - v-bind: 【简写:】
表示通知vue在渲染的 DOM 标签时,将bind绑定的属性 和 Vue 实例data中同名属性值保持一致,单项绑定 - v-model:
与v-bind类似, 不过数据可同步改动,双向绑定 - v-html= {{属性}}
会原样输出数据属性,如果说数据是带有html格式的数据时,此时需要使用v-html指令 - v-if= 【 v-else-if= 】
判断指令 - v-for=
循环指令
v-for="item in arr"
for="(item, index) in arr"
- v-on【简写@】
事件绑定指令, 可缩写成@,方法写在methods:{//所有vue实例属性,所有vue相关的函数都在这里定义}
里
(四)、vue事件
- methods: { choseClick: function (){...}}
@click .. 事件函数 -
$event
事件信息封装对象: 使用 $event 标记 - e.currentTarget
获取事件源 - choseClick($event,u.id,u.name)"
事件传参,调用事件函数传入参数
choseClick:function (e, id, name) {...}
(五)、vue的属性
- el : "#app"
用来指示vue编译器从什么地方开始解析 vue的语法 - data:
用来组织从view中抽象出来的属性,可以说将视图的数据抽象出来存放在data中,data:{ arr:[1,2,3] }
- methods:
放置页面中的业务逻辑,js函数一般都放置在methods中 - filters:
vue过滤器集合
dataFormat:function () {
},
sexFilter:function (sex) {
return sex == 0? '女':'男'
}
- mounted:function(){...}
是一个函数,用来初始化,在vue实例创建完成后被立即调用(html加载完成后执行)
(六)、前后端分离
浏览器怎么发起请求
![](https://img.haomeiwen.com/i11635091/46e98c11f4c73f6d.png)
vue怎么接受处理请求
浏览器访问页面,vue的mounted 属性初始化发起跨域异步请求,调用后端暴露的接口,控制器处理返回json数据data,使用js加工,设置到vue的data:属性,通过v-for循环取出数据
vue怎么发起异步请求
mounted 属性初始化操作,发起$.get异步请求
接口服务器怎么接受并处理异步请求
控制器通过$get异步请求,调用后端暴露的接口,再通过业务方法查询出list数据后返回json数据给前端
vue怎么处理接口返回json格式结果
通过控制器出来返回json数据,使用异步请求的回调函数来接收参数,设置进vue的data属性里
(七 )、其他
跨域
- mvc配置方式
@SpringBootApplication
public class MongodbApplication implements WebMvcConfigurer {
//跨域访问
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
//重写父类提供的跨域请求处理的接口
public void addCorsMappings(CorsRegistry registry) {
//添加映射路径
registry.addMapping("/**")
//放行哪些原始域
.allowedOrigins("*")
//是否发送Cookie信息
.allowCredentials(true)
//放行哪些原始域(请求方式)
.allowedMethods("GET", "POST", "PUT", "DELETE","OPTIONS")
//放行哪些原始域(头部信息)
.allowedHeaders("*")
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
.exposedHeaders("Header1", "Header2");
}
};
}
public static void main(String[] args) {
SpringApplication.run(MongodbApplication.class,args);
}
}
- 注解方式
@CrossOrigin(origins = "http://localhost:8888")
表示只允许这一个url可以跨域访问这个controller
七、mongodb的使用
(一)、简介
-
非关系型数据库(nosql数据库)中的文档型关系数据库
以bson(升级版json格式)结构存储数据 -
非关系型数据库特点
1.数据模型比较简单.(主要)
2.需要灵活性更强的应用系统
3.对数据库性能要求较高(主要)
4.不需要高度的数据一致性(主要)
5.对于给定key,比较容易映射复杂值的环境. -
mongodb特点
1:JSON结构和对象模型接近,开发代码量少
2:JSON动态模型意味着更容易响应新的业务需求
3:复制集提供了 99.999%高可用
4:分片架构支持海量数据无缝扩容 -
每个文档大小不能超过16MB
(二)、 MongoDB范式化与反范式化
-
范式化:将数据分散到多个不同的集合,不同集合之间可以相互引用数据。如果要修改数据,只需修改保存这块数据的文档就行。但是MongoDB没有连接(join)工具,所以在不同集合之间执行连接查询需要进行多次查询。
-
反范式化:将每个文档所需的数据都嵌入在文档内部。每个文档都有自己的数据副本,而不是所有文档共同引用一个数据副本。但是如果数据发生变化,那么所有相关文档都需要进行更新。
-
范式化能够提高数据写入速度,反范式化能够提高数据读取速度。
(三)、Spring Data
Spring Data方法命名规范
关键字 | 例子 | JPQL |
---|---|---|
And | findByNameAndAge(String name, Integer age) | where name = ? and age = ? |
Or | findByNameOrAge(String name, Integer age) | where name = ? or age = ? |
Is | findByName(String name) | where name = ? |
Between | findByAgeBetween(Integer min, Integer max) | where age between ? and ? |
LessThan | findByAgeLessThan(Integer age) | where age < ? |
LessThanEqual | findByAgeLessThanEqual(Integer age) | where age <= ? |
GreaterThan | findByAgeGreaterThan(Integer age) | where age > ? |
GreaterThanEqual | findByAgeGreaterThanEqual(Integer age) | where age >= ? |
After | 等同于GreaterThan | |
Before | 等同于LessThan | |
IsNull | findByNameIsNull() | where name is null |
IsNotNull | findByNameIsNotNull() | where name is not null |
Like | findByNameLike(String name) | where name like ? |
NotLike | findByNameNotLike(String name) | where name not like ? |
StartingWith | findByNameStartingWith(String name) | where name like '?%' |
EndingWith | findByNameEndingWith(String name) | where name like '%?' |
Containing | findByNameContaining(String name) | where name like '%?%' |
OrderByXx[desc] | findByIdOrderByXx[Desc] (Long id) | where id = ? order by Xx [desc] |
Not | findByNameNot(String name) | where name != ? |
In | findByIdIn(List<Long> ids) | where id in ( ... ) |
NotIn | findByIdNotIn(List<Long> ids) | where id not in ( ... ) |
True | findByXxTrue() | where Xx = true |
False | findByXxFalse() | where Xx = false |
IgnoreCase | findByNameIgnoreCase(String name) | where name = ? (忽略大小写) |
(四)、spring data jpa
https://www.cnblogs.com/chenglc/p/11226693.html
八、 旅游攻略
后端
攻略分类
添加
修改
删除
攻略主题
添加
修改
删除
攻略明细
-
1:添加
攻略明细保存.png
1>分组下拉框(共享数据结构分析/组装)
2>富文本编辑器
使用ckeditor工具
@RequestMapping("/uploadImg_ck")
@ResponseBody
public Map<String, Object> upload(MultipartFile upload, String module){
Map<String, Object> map = new HashMap<String, Object>();
String imagePath= null;
if(upload != null && upload.getSize() > 0){
try {
//图片保存, 返回路径
imagePath = UploadUtil.uploadAli(upload);
//表示保存成功
map.put("uploaded", 1);
map.put("url",imagePath);
}catch (Exception e){
e.printStackTrace();
map.put("uploaded", 0);
Map<String, Object> mm = new HashMap<String, Object>();
mm.put("message",e.getMessage() );
map.put("error", mm);
}
}
return map;
}
3>攻略添加注意冗余字段
-
2:攻略编辑
分组下拉框.png
1>部分字段更新
2>下架
前端
前端目的地明细
1>吐司
2>目的地下分类概况/明细
![](https://img.haomeiwen.com/i11635091/f49491a279ac6fcd.png)
3>目的地下点击量前3的攻略
4>攻略明细
九、 旅游日记
后端
1>游记表设计
2>游记的列表
3>游记查看
4>游记的审核/下架
前端
目的地明细中-游记
![](https://img.haomeiwen.com/i11635091/bb8f70e481b3b75a.png)
1>带范围条件查询分析
2>带范围条件查询实现
3>游记首页
游记的添加
游记明细
1>明细查看
2>吐司
3>点击量前3 攻略/游记
十、评论
前端
评论类型
盖楼式
微信评论式
步骤
- 1:攻略评论
1>添加
// 添加评论
@RequireLogin
@PostMapping("addComment")
private Object addComment(StrategyComment comment, @UserParam UserInfo userInfo) {
//评论数+1
strategyStatisRedisService.increaseReplynum(comment.getStrategyId());
//属性拷贝,参数1: 源数据 , 参数2: 目标数据对象
//底层原理:使用内省方式,同名属性进行赋值
BeanUtils.copyProperties(userInfo, comment);
comment.setUserId(userInfo.getId());
strategyCommentService.save(comment);
return JsonResult.success();
}
2>查询
3>点赞
@Override
public void commentThumb(String cid, String uid) {
// 获取评论操作对象
StrategyComment comment = this.get(cid);
// 获取评论数
int thumbupnum = comment.getThumbupnum();
// 获取点赞用户id集合
List<String> userlist = comment.getThumbuplist();
//1:判断当前用户是否点赞过
if (!userlist.contains(uid)) {
// 点赞
comment.setThumbupnum(thumbupnum + 1);
userlist.add(uid);
comment.setThumbuplist(userlist);
} else {
// 取消点赞
comment.setThumbupnum(thumbupnum - 1);
userlist.remove(uid);
comment.setThumbuplist(userlist);
}
// 2:保存修改后的对象
repository.save(comment);
}
- 2:游记的评论
1>添加
2>查询
3>表情
使用正则校验后
以(大笑小蜂)为key遍历数组取得值,值为表情对应路径
var matchArr = str.match(reg); //[(大笑小蜂), 大笑小蜂),(得意小蜂)]
十一、 数据统计
使用redis初始化统计数据后,回显到前端页面上,前端直接操作redis来修改统计数据,redis上的数据,经过spring定时任务持久化到monodb数据库中
统计的实体
/**
* 攻略redis中统计数据
* 运用模块:
* 1:数据统计(回复,点赞,收藏,分享,查看)
*/
@Getter
@Setter
public class StrategyStatisVO implements Serializable {
private Long strategyId; //攻略id
private int viewnum; //点击数
private int replynum; //攻略评论数
private int favornum; //收藏数
private int sharenum; //分享数
private int thumbsupnum; //点赞个数
}
![](https://img.haomeiwen.com/i11635091/f022e76bd4827e7d.png)
-
点赞数
顶实现.png
-
收藏数
用户攻略收藏统计.png
-
回复数
回复数增加方法写在添加评论里 -
阅读数
阅读数统计.png
redis的初始化
1>分析
![](https://img.haomeiwen.com/i11635091/53a820f303109e5b.png)
2>spring监听器
3>逻辑实现
// spring容器启动好之后马上执行的方法
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
System.out.println("=========================== vo对象的初始化 begin ====================");
// 1:查询mongodb中的所有攻略
List<Strategy> list = strategyService.list();
// 2: 遍历这些攻略对象封装到统计vo对象
for (Strategy strategy : list) {
//第一次初始化完成之后, 如果页面进行操作, redis数据会发生变动, 在没有持久化入库前,再次启动
//如果不做跳过处理,会出现旧数据覆盖信息数据
// 如果redis已经存在vo对象,直接跳过
if(strategyStatisRedisService.isVoExists(strategy.getId())){
continue;
}
// 3:添加到redis中
StrategyStatisVO vo = new StrategyStatisVO();
BeanUtils.copyProperties(strategy, vo);
vo.setStrategyId(strategy.getId());
strategyStatisRedisService.setStrategyStatisVO(vo);
}
System.out.println("=========================== vo对象的初始化 end ====================");
}
redis的持久化
1>分析
![](https://img.haomeiwen.com/i11635091/07584977c8975406.png)
2>spring定时器
3>逻辑实现
使用Cron表达式实现定时
@Scheduled(cron="0/10 * * * * ?")
public void dowWork() {
System.out.println("=========== vo对象持久化-begin ==========" + new Date());
// 1:获取所有vo对象
List<StrategyStatisVO> vo = strategyStatisRedisService.queryByPattern(RedisKeys.STRATEGY_STATIS_VO.getPerfix());
// 2:遍历对象集合,执行持久化
for (StrategyStatisVO strategyStatisVO : vo) {
strategyStatisRedisService.saveVo(strategyStatisVO);
}
System.out.println("=========== vo对象持久化-end ==========");
}
十二、 网站首页
banner
后端
- 添加
- 删除
- 修改
使用阿里云的oss对象存储存储上传的图片
前端
- 查询前5条banner
- 查询一条最热游记
十三、 elasticsearch的使用
![](https://img.haomeiwen.com/i11635091/6bc812108d07a73b.png)
(一)、kibana
ES操作客户端-kibana
操作
#添加
#设置5个片区
#设置1个备份
PUT my_index
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
}
}
#### 新增和替换文档
语法:PUT /索引名/类型名/文档ID
{
field1: value1,
field2: value2,
...
}
注意:当索引/类型/映射不存在时,会使用默认设置自动添加
ES中的数据一般是从别的数据库导入的,所以文档的ID会沿用原数据库中的ID
索引库中没有该ID对应的文档时则新增,拥有该ID对应的文档时则替换
需求1:新增一个文档
需求2:替换一个文档
每一个文档都内置以下字段
>_index:所属索引
>
>_type:所属类型
>
>_id:文档ID
>
>_version:乐观锁版本号
>
>_source:数据内容
#### 查询文档
语法:
根据ID查询 -> GET /索引名/类型名/文档ID
查询所有(基本查询语句) -> GET /索引名/类型名/_search
需求1:根据文档ID查询一个文档
需求2:查询所有的文档
查询所有结果中包含以下字段
>took:耗时
>
>_shards.total:分片总数
>
>hits.total:查询到的数量
>
>hits.max_score:最大匹配度
>
>hits.hits:查询到的结果
>
>hits.hits._score:匹配度
#### 删除文档
语法:DELETE /索引名/类型名/文档ID
注意:这里的删除并且不是真正意义上的删除,仅仅是清空文档内容而已,并且标记该文档的状态为删除
需求1:根据文档ID删除一个文档
需求2:替换刚刚删除的文档
## 高级查询
Elasticsearch基于JSON提供完整的查询DSL(Domain Specific Language:领域特定语言)来定义查询。
基本语法:
GET /索引名/类型名/_search
一般都是需要配合查询参数来使用的,配合不同的参数有不同的查询效果
参数配置项可以参考博客:<https://www.jianshu.com/p/6333940621ec>
### 结果排序
参数格式:
{
"sort": [
{field: 排序规则},
...
]
}
排序规则:
asc表示升序
desc:表示降序
没有配置排序的情况下,默认按照评分降序排列
分页查询
参数格式:
{
"from": start,
"size": pageSize
}
需求1:查询所有文档按照价格降序排列
需求2:分页查询文档按照价格降序排列,显示第2页,每页显示3个
#查询所有
GET _cat/indices
#查询单个
GET my_index
#删除
DELETE my_index
倒排索引
![](https://img.haomeiwen.com/i11635091/4227ae41ddb20778.png)
Spring Data Elasticsearch
依赖
<!--SpringBoot整合Spring Data Elasticsearch的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
实体类
/**
@Document:配置操作哪个索引下的哪个类型
@Id:标记文档ID字段
@Field:配置映射信息,如:分词器
*/
@Getter@Setter@ToString
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName="shop_product", type="shop_product")
public class Product {
@Id
private String id;
@Field(analyzer="ik_max_word",searchAnalyzer="ik_max_word", type=FieldType.Text)
private String title;
private Integer price;
@Field(analyzer="ik_max_word",searchAnalyzer="ik_max_word", type=FieldType.Text)
private String intro;
@Field(type=FieldType.Keyword)
private String brand;
}
配置信息
#application.properties
# 配置集群名称,名称写错会连不上服务器,默认elasticsearch
spring.data.elasticsearch.cluster-name=elasticsearch
# 配置集群节点
spring.data.elasticsearch.cluster-nodes=localhost:9300
十四、主页搜索
es数据初始化操作
@GetMapping("/dataInit")
public Object dataInit() {
// 用户初始化
List<UserInfo> us = userInfoService.list();
for (UserInfo userInfo : us) {
UserInfoEs userInfoEs = new UserInfoEs();
BeanUtils.copyProperties(userInfo, userInfoEs);
userInfoEsService.save(userInfoEs);
}
// 目的地初始化
List<Destination> dsts = destinationService.list();
for (Destination destination : dsts) {
DestinationEs destinationEs = new DestinationEs();
BeanUtils.copyProperties(destination, destinationEs);
destinationEsService.save(destinationEs);
}
// 游记初始化
List<Travel> trs = travelService.list();
for (Travel travel : trs) {
TravelEs travelEs = new TravelEs();
BeanUtils.copyProperties(travel, travelEs);
travelEsService.save(travelEs);
}
// 攻略初始化
List<Strategy> sts = strategyService.list();
for (Strategy strategy : sts) {
StrategyEs strategyEs = new StrategyEs();
BeanUtils.copyProperties(strategy, strategyEs);
strategyEsService.save(strategyEs);
}
return "ok";
}
关键字搜索
1>目的地精确搜索
// 查询目的地
private Object searchDest(SearchQueryObject qo) {
SearchResultVo result = new SearchResultVo();
// 1.判断目的地是否存在
// es: 通过destName匹配,然后找到ids集合,再通过ids查询mongodb得到数据集合
// mongodb: 先通过destName匹配,得到数据集合
Destination dest = destinationService.findByName(qo.getKeyword());
// 2.如果存在,查询该目的地下所有攻略,游记
if (dest != null) {
// 攻略
List<Strategy> sts = strategyService.findByDestName(dest.getName());
result.setStrategys(sts);
// 游记
List<Travel> ts = travelService.findByDestName(dest.getName());
result.setTravels(ts);
// 用户
List<UserInfo> us = userInfoService.findByCity(dest.getName());
result.setUsers(us);
result.setTotal(sts.size() + ts.size() + us.size() + 0L);
}
// 3:如果不存在,页面提示
Map<String, Object> map = new HashMap<>();
map.put("qo", qo);
map.put("dest", dest);
map.put("result", result);
return JsonResult.success(map);
}
2>其他全文搜索
// --------- 全文搜索 ---------
// 查询攻略
private Object searchStrategy(SearchQueryObject qo) {
Page<StrategyEs> page = searchService.searchWithHighlight(StrategyEs.INDEX_NAME,
StrategyEs.TYPE_NAME, StrategyEs.class, qo, "title", "subTitle", "summary");
return JsonResult.success(new ParamMap().put("page",page ).put("qo", qo));
}
// 查询游记
private Object searchTravel(SearchQueryObject qo) {
Page<TravelEs> page = searchService.searchWithHighlight(TravelEs.INDEX_NAME,
TravelEs.TYPE_NAME, TravelEs.class, qo, "title", "summary");
return JsonResult.success(new ParamMap().put("page",page ).put("qo", qo));
}
// 查询用户
private Object searchUser(SearchQueryObject qo) {
Page<UserInfoEs> page = searchService.searchWithHighlight(UserInfoEs.INDEX_NAME,
UserInfoEs.TYPE_NAME, UserInfoEs.class, qo, "city", "nickname");
return JsonResult.success(new ParamMap().put("page",page ).put("qo", qo));
}
十五、一问一答
mongodb的事务
mongodb3.0开始WiredTiger引擎可以针对单个文档来保证ACID特性, MongoDB 4.0, 支持复制集多文档事务,支持多文档ACID特性
Redis支持的数据类型?
支持string、hash、list(元素允许重复,有顺序区别)、set(不允许重复,无序集合)、zset(有序集合,分数score却可以重复。)