美文网首页
骡窝窝项目总结

骡窝窝项目总结

作者: 建国同学 | 来源:发表于2020-06-27 21:10 被阅读0次

    一、骡窝窝项目概要

    技术路线

    1,数据库:mongodb + elasticsearch;
    2,持久化层:mongodb+Redis (缓存);
    3,业务层:Springboot;
    4,Web:SpringMVC;
    5,前端:
    管理后台:jQuery+Bootstrap3
    前端展示:vue +jquery + css;

    项目拆分

    拆分便于项目的维护与扩展,更方便于多服务的部署。

    构建多模块项目

    项目拆解

    parent项目如何对依赖做管理:

    • 1 :如果所有子项目都需要依赖某个jar包,将
      这个jar配置parent项目的<dependances>
      所有子项目可以共享

    • 2 :如果某些子项目需要依赖某个jar ,某些子
      不需要依赖jar ,将这个jar配置parent项目的
      < ManagementDependances>配置配置jar
      不会再所有子项目共享, parent仅仅是管理这,个依赖版本,需要引用jar的子项目需要单独引入jar包,但是可以不写版本信息。

    • 3.如果就某一个项目需要依赖某个jar,该子项目自己引入jar即可。

    二、 用户注册

    注册分析.png

    1、手机号码格式

    使用正则表达式验证

    2、手机号的唯一验证

    查询用户表的手机号字段是否存在数据库中

    3、注册参数校验

    校验参数是否为空值

    4、短信验证码

    验证码发送分析.png
    1. 页面倒计时

    2. 使用短信网关api接口(京东万象)


      短信发送分析.png 令牌登录分析.png
    3. 短信验证码存入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 字符串命令

    redis基本命令

    hash命令

    java中使用spring封装的redis对象StringRedisTemplate来操作时,Mp<string, Map<string, ?>> 重复会覆盖,但hashvalue的值会累加


    redis操作hash数据

    list命令

    redis操作list数据

    (四)、Redis内存淘汰机制及过期Key处理

    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文件添加操作日志记录

    四、用户登录

    跨域访问

    解决跨域访问问题
    跨域请求原理:

    1. 浏览器如果发起异步请求,发现请求路径url是跨域请求,浏览器会使用POTIONS方式发起总攻url请求
      ,携带是否许可访问请求数据
    2. 服务器接收这个请求,进行对比根据之前的跨域配置判断是否允许这个ip端口访问呢,如果允许响应允许访问信息,如果不允许,响应会不允许
    3. 浏览器接收这个服务器反馈,根据反馈的结果来决定是否发起真正请求
      如果允许跨域,以真正请求方式发起,如果不允许跨域,返回时不允许跨域的异常
    步骤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令牌方式登录流程

    令牌登录分析.png

    登录控制

    在配置类里配置CheckLoginInterceptor拦截器

    定义:CheckLoginInterceptor
    
    配置:CheckLoginInterceptor
    
    在主配置类中implements WebMvcConfigurer
     @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(checkLoginInterceptor())
                    .addPathPatterns("/**");
        }
    

    使用自定义注解@RequireLogin的方式区分需要拦截登录的控制器

    当前用户注入

    1. 自定义参数解析器
    2. 添加自定义参数解析器
      使用addArgumentResolvers 在启动类里注册该参数解析器

    五、 目的地

    后端

    区域管理

    1. 列表
      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);
        }
    
    1. 添加
      带搜索框下拉框bootstrap-select
    2. 编辑
      回显
    $('#refIds').selectpicker('val', refIds);
    $('#refIds').selectpicker('refresh');
    
    1. 查看
    2. 删除
    3. 热门
      改变区域表里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

    (二)、生命周期图示

    下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。

    Vue 实例生命周期

    (三)、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加载完成后执行)

    (六)、前后端分离

    浏览器怎么发起请求

    前后端分离操作流程.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>目的地下分类概况/明细

    分类概况.png

    3>目的地下点击量前3的攻略
    4>攻略明细

    九、 旅游日记

    后端

    1>游记表设计
    2>游记的列表
    3>游记查看
    4>游记的审核/下架

    前端

    目的地明细中-游记

    带范围条件查询.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; //点赞个数
    }
    
    缓存与统计对象分析.png
    • 点赞数


      顶实现.png
    • 收藏数


      用户攻略收藏统计.png
    • 回复数
      回复数增加方法写在添加评论里

    • 阅读数


      阅读数统计.png

    redis的初始化

    1>分析

    初始化.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>分析


    持久化.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的使用

    image.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
    

    倒排索引

    image.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却可以重复。)

    相关文章

      网友评论

          本文标题:骡窝窝项目总结

          本文链接:https://www.haomeiwen.com/subject/imyvfktx.html