1 Chapter01

1.1 开发原则

01.常见问题
    a.代码行数过长
        团队内部人事变动、代码方法行数过长、if else缝合怪、SQL存储过程
    b.代码逻辑不明
        开发人员:逻辑双重否定表肯定、需求理解偏差难以修改、类名方法名命名不规范
        产品经理:频繁变更产品的需求
    c.变量含义标识符:名词性词组
        变量名             目标词 + 动词(的过去分词) + [状语] + [目的地]
        DataGotFromSD       Data        Got            Form     SD          从SD中获取数据
        DataDeleteFromSD    Data        Deleted        Form     SD          从SD中删除数据
    d.函数含义标识符:动词性词组
        变量名             动词(一般现在时)+ 目标词 + [状语] + [目的地]
        GetDataFromSD       Get               Data      Form    SD          从SD中获取数据
        DeleteDataFromSD    Delete            Data      Form    SD          从SD中删除数据
    e.常见词汇区分
        state               进行变更的并且有迁移条件的短暂状态
        status              在特定时间,事物所处的状态               Http Status
        Registry            不同镜像的不同版本
        Repository          同一个镜像的不同版本
        Authorization       授权,关于“权限”的内容,授权委托
        Authentication      认证,关于“登录”的内容,身份验证
        Query               在结构性的数据中查找,比较明确的查询条件,相对来说可以比较快给出答案
        Search              在非结构性数据中查找,比较模糊的查询条件,相对来说需要花费比较长的时间
        Fault               不正确的系统内部状态
        Failure             系统不能够完成预期的任务

02.花叶论
    a.代码类型
        数据校验
        业务逻辑
        数据转换(DTO转换VO)
        数据库交互(查询与持久化)
    b.凸显主干逻辑
        a.Converter(转换器)
            简单转换:通过调用BeanUtils类进行实体转换
            复杂转换:通过手动的set、get来进行实体转换,但往往会掩盖主干逻辑,让代码逻辑不清晰,故建议Converter类
        b.manager层(将Mapper层简单逻辑移动到manager层)
            阿里规范:对Jpa、MyBatis等持久层部分方法进行简单封装
            适用场景:对于Mapper层简单逻辑(set赋值操作,查询条件筛选、查询结果判断),较高重复性,封装到manager层
        c.方法简单封装(函数式编程)
            函数式编程:简化复杂方法封装,不仅仅是stream流和lambda表达式
            函数式编程类型:Function 类型(传入一个bean 返回另外一个bean)
            Consumer 类型(传入一个bean 无返回值)
            Predicate 类型(传入一个bean 返回布尔值)
            Supplier 类型(没有入参,有出参)
        d.推荐写法
            public User getUser(Consumer<User> consumer){
                User user=new User();
                consumer.accept(user);
                user=userMapper.getUser(user);
                return user;
            }
            public void doSomething1(){
                User user=getUser(user->{user.setId(1L)});
            }
            public void doSomething2(){
                User user=getUser(user->{user.setName("xxx")});
            }

03.日志和注释
    a.日志
        前提:会不会有人去查看这段日志?这段日志有没有用?
        搜索:关键词
        日志:关键词不与其他日志重复,不要过长,便于搜索才是王道
        建议打日志的地方:①数据更新(我们有必要知道写库的数据是不是正确的数据)
                        ②条件分支(便于我们分析业务走的哪一条逻辑)
                        ③批量写库(打上数据量大小的日志,便于我们分析性能瓶颈)
        建议打日志的提示:①并不是上述3处地方都应该打日志,有时候可能通过一两句日志就能分析出问题所在,日志会显得多余
                        ②打完日志之后应该在本地环境追溯一下,看看这些日志是否能读懂,是否有必要,是否少了重要参数
    b.注释
        a.类、类属性、类方法的注释必须使用Javadoc规范,使用/**内容*/格式,不得使用// xxx 方式
            说明:使用Javadoc正确输出相应注释,调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义
        b.所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、异常说明外,还必须写明相应功能
            说明:对子类的实现要求,或者调用注意事项,请一并说明
        c.所有的类都必须添加创建者和创建日期
            说明:日期的设置统一为 yyyy/MM/dd 的格式
        d.方法内部单行注释,在被注释语句上方另起一行,使用//注释;方法内部多行注释使用/* */注释,注意与代码对齐
            /**
            * require:产品需求的原文 + 日期
            */
            pulic void test(){
                /** 1. 从excel 获取 vo*/
                // 获取成员信息
                // 获取项目vo
                // 获取任务vo
                /** 2. 插入数据 */
                /** 3.写入异常信息 */
            }
        e.推荐
            英文注释:如果英文表达能力较弱,建议使用中文注释,专有名词与关键字保持英文原文即可,如TCP
            更新注释:修改代码的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改
            删除引用:在类中删除未使用的任何字段和方法;在方法中删除未使用的任何参数声明与内部变量
        f.参考
            注释代码:①后续会恢复此段代码逻辑(自选);②无用(直接删除)
            注释要求:①供自己阅读代码;②为继任者提供阅读
            注释极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担
            注释标记:待办事宜(TODO)(标记人,标记时间,[预计处理时间])

04.开发原则
    a.面向接口编程
        开闭原则原则(OCP)(实现:功能扩展):软件实体应尽量在不修改原有代码的情况下,进行功能扩展
        迪米特的原则(LOD)(实体:杜绝逻辑):一个软件实体应当尽可能少地与其他实体发生相互作用
        单一职责原则(SRP)(方法:代码复用):一个方法只做一件事情,承担职责越多,复用可能性越小
        接口隔离原则(ISP)(接口:依赖最小):推荐使用多个专门接口,而非单一总接口,减少不需要的接口(吃、睡、X)
        里氏替换原则(LSP)(多态:父子类):所有引用父类的地方都替换成其子类,系统应该仍然可以正常工作
        依赖倒置原则(DIP)(多态:接口类、抽象类):高层模块不应该依赖低层模块,两者都应该依赖其抽象层
    b.方法命名规范
        阿里开发手册  前缀与后缀的规范
    c.代码提交及版本控制
        团队约定的commit大致规范、gitflow流程图
    d.代码规范的工具
        Mapstruct Support、checkStyle、Git Flow Integration、Git Commit Template

05.阿里规约:项目足够简单,掌握细度大小
    a.图示
        终端显示层                       开发API层
        请求处理层(Controller层)           ↓
        --------------------------------------------------------
        业务逻辑层(Service层)
                ↓                       通用逻辑层(Manager层)
        --------------------------------------------------------
        数据持久层(DAO层)               第三方服务
        数据存储系统                     外部数据接口
    b.应用分层
        POJO(Plain Ordinary Java Object):简单JAVA对象,VO/DTO/BO/DO统称,禁止命名成xxxPOJO
        默认上层依赖于下层,箭头关系表示可直接依赖,如:开放API层可以依赖于Controller层,也可以直接依赖于Service层
        ---------------------------------------------------------------------------------------------------
        终端显示层:各个端的模板渲染并执行显示的层。当前主要是velocity渲染,JS渲染,JSP渲染,移动端展示等
        开放API层:可直接封装Service接口暴露成RPC接口;通过Web封装成http接口;网关控制层等
        Controller层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等
        ---------------------------------------------------------------------------------------------------
        Service层:相对具体的业务逻辑服务层
        Manager层:通用业务处理层,1)对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口
                                 2)对Service层通用能力的下沉,如缓存方案、中间件通用处理
                                 3)与DAO层交互,对多个DAO的组合复用
        ---------------------------------------------------------------------------------------------------
        Data Access Object层:数据访问层,包括DAO类、DAO接口、DAO接口实现类、DAO工厂类,实现增删改查
        第三方服务:包括其它部门RPC服务接口,基础平台,其它公司的HTTP接口,如淘宝开放平台、支付宝付款服务等
        外部数据接口:外部(应用)数据存储服务提供的接口,多见于数据迁移场景中
    c.业务流程
        Template:用户发出请求(填写表单),表单的数据被展示层匹配为VO
        Controller:展示层把VO转换为服务层对应方法所要求的DTO,提交给服务层
        Service:服务层先将DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务
        DAO:数据层再将DO转换为持久层对应的PO,调用持久层的持久化方法,把PO传递持久化方法,完成持久化操作
    d.对象范围
        Controller  输出:VO
        Controller  输入:DTO / BO / DO
        ---------------------------------------------------------------------------------------------------
        Service     输出:DTO / BO / DO
        Service     输入:DTO / BO / DO
        ---------------------------------------------------------------------------------------------------
        Manager     输出:DTO / BO / DO
        Manager     输入:DTO / BO / DO
        ---------------------------------------------------------------------------------------------------
        DAO         输出:DO
        DAO         输入:Field
    e.对象命名
        VO(View Object):展示对象,Controller向模板渲染引擎层的传输对象,数据脱敏,去掉用户隐私数据
        ---------------------------------------------------------------------------------------------------
        DTO(Data Transfer Object):传输对象,Service或Manager传输对象,通常OpenAPI与其他项目交互对象
        BO(Business Object):业务对象,可以由Service层输出的封装业务逻辑的对象,包含对DAO、RPC调用,DO/DTO/VO转换
        ---------------------------------------------------------------------------------------------------
        DO(Domain Object)/PO(Persistent Object):领域对象/持久对象,与数据库表结构一一对应,通过DAO层向上传输
    f.对象理解
        VO:页面数据,【xxxVO,xxx一般为网页名称】【05字段】,一般并不包括隐私数据
        ---------------------------------------------------------------------------------------------------
        DTO:OpenAPI,【xxxDTO,xxx一般为业务名称】【10字段】,若在其他项目中DTO对应界面显示,那么DTO也可以作为VO使用
        BO:个人简历,【xxxBO,xxx一般为业务名称】【组合字段】,包括隐私数据、教育经历、工作经历、社会关系
        ---------------------------------------------------------------------------------------------------
        DO:隐私数据,【xxxDO,xxx一般为数据表名】【20字段】
        DO:教育经历,【xxxDO,xxx一般为数据表名】【20字段】
        DO:工作经历,【xxxDO,xxx一般为数据表名】【20字段】
        DO:社会关系,【xxxDO,xxx一般为数据表名】【20字段】

1.2 项目解读

01.术语统计(tree /f >tree.txt、textlint demo.md、textlint --fix demo.md)
    a.项目
        - `pom.xml` :项目依赖,【】
        - `spy.properties` :配置文件,【】
        - `application.yml` :配置文件,【】
        - `Application.java`:项目启动,【】
    b.项目配置
        - `AuthFilter.java` :过滤器,【】
        - `ViewSyncTask.java` :定时器,【】
        - `ValidationUtil.java` :工具类,【】
        - `FreemarkerConfig.java` :配置类,【】
    c.项目分层
        - `UserMessageVo.java` :实体类,【】
        - `AuthController.java` :控制层,【】
        - `CategoryService.java`:业务层接口,【】
        - `CategoryServiceImpl.java`:业务层实现,【】
        - `UserCollectionMapper.java` :数据层接口,【】
        - `UserCollectionMapper.xml` :数据层实现,【】
    d.前端渲染
        - `im.js` :js文件,【】
        - `common.ftl` :模板引擎,【】
        - `/res/mods/index.js` :源码可知,【】

02.术语表达
    a.复杂表达-单独表达
        ### 6.2 本周热议的【基本原理】:利用Redis的zet有序集合实现
        - 缓存热评文章——哈希表 Hash
        - 评论数量排行——有序列表 sortedSet:ZADD(添加)、ZREVRANGE(展示)、ZUNIONSTORE(并集)
            - ZADD key score member [[score member] [score member] ...]
            - ZREVRANGE key start stop [WITHSCORES]
            - 查看排行榜
            - 添加/删除评论
    b.复杂表达-混合表达
        - 实现逻辑:
            - 自增/自减评论数
            - 更新这篇文章的缓存时间,并更新这篇文章的基本信息
            - 对【近 7 天文章】重新做并集运算(zUnionAndStore), 并使用根据评论量的数量从大到小进行展示(zrevrange)
        - `DirectiveHandler.java`:配置类,【配置标签】
        - `PostsTemplate.java` :工具类,【开发标签】
        - `FreemarkerConfig.java` :配置类,【注册标签】
    c.说明模板
        - Chapter 04
            - [](https://github.com/halavah/blog/tree/master/doc/Chapter04/)
            - [](https://github.com/halavah/blog/tree/master/doc/Chapter04/)
            - [](https://github.com/halavah/blog/tree/master/doc/Chapter04/)
            - [](https://github.com/halavah/blog/tree/master/doc/Chapter04/)
    d.通用模板
        # 1. 集成 WeSocket 实现用户评论-即时通讯
        ## 1.1 集成 WebSocket 环境
        ## 1.2 配置 WebSocket 环境
        ## 1.3 使用 WebSocket 通讯
    e.提交说明
        v1_init:init project                                                                 v1.0
        v3_other:format_doc                                                                  无
        v2_center:我的消息(查询消息、删除单个消息、删除全部消息)、消息弹窗                     v2.2
        v3_detail:添加文章(添加/编辑/提交)、超级用户(删除/置顶/精华)、博客详情(删除/评论)   v3.2
    f.文档检查
        tree /f >tree.txt
        textlint demo.md
        textlint --fix demo.md

03.术语语法:MD严格换行(两段之间必须空一行;插入两个空格和一个换行符;直接插入<br/>标签)
    a.目录
        [TOC]
    b.标题
        # 一级标题
        ## 二级标题
        ### 三级标题
        #### 四级标题
        ##### 五级标题
        ###### 六级标题
    c.加粗/斜体/粗斜体
        *斜体文本*         _斜体文本_
        **加粗文本**       __加粗文本__
        ***粗斜体文本***   ___粗斜体___
    d.删除线/下划线
        ~~删除线文本~~
        <u>下划线文本</u>
    e.有序列表
        1. 第一点
        2. 第二点
        3. 第三点
    f.无序列表
        - 减号列表
        * 星号列表
        + 加号列表
    g.混合列表
        - abcd
            1. abcd
            2. abcd
            3. abcd
        - abcd
        - abcd
    h.引用
        > 一级引用文本
        >> 二级引用文本
        >>> 三级引用文本
    i.脚注
[^脚注名]: [^脚注名]: [^脚注名]: [^脚注名]: [^脚注名]:[^脚注名]:脚注内        --注意:脚注的冒号:后不应加入空格
    j.上下角标
        <sup>|</sup>                                                     --上角标   引文<sup>[1]</sup>
        <sub>|</sub>                                                     --下角标   C<sub>2</sub>H<sub>5</sub>OH
    k.居左/居中/居右
        <p align="center">|</p>                                          --文字居中
        <center>|<\center>                                               --代码居中
    l.插入图片
        ![]()                                                            --![1.png](../assets/1.png)
    m.插入链接
        []()                                                             --[简书](https://www.jianshu.com "创作你的创作")
    n.插入表格,表头和表体使用-分割,其中-的数量应≥3
        | 左对齐 |  居中  | 右对齐 |
        | :----- | :----: | -----: |
        | left   | center |  right |
        | left   | center |  right |
        | left   | center |  right |
    o.选择框
        - [ ] 未选中
        - [x] 选中
    p.页内快速跳转
        # title1                                               --页内的快速跳转需要有标题(h1~h6)支持
        [Back to title 1](#title1)                             --插入到某个标题的跳转链接
    q.分割线
        ---                                                    --至少3个减号,文本与分割线之间应加入一个空行
    r.改变字符颜色
        <font color="色号或单词">文本</font>
        <font color="black">黑色文字</font>
        <font color="#000000">黑色文字</font>
    s.标记字符
        \\ \* \+ \- \` \_
    t.代码块
        `abcd`
        ```abcd```
    u.注释(better-comments)
        // ! 红色的高亮注释
        // ? 蓝色的高亮注释
        // * 绿色的高亮注释
        // todo 橙色的高亮注释
        // // 灰色带删除线的注释
        // 普通的注释
        /**
          // ! 红色的高亮注释
          // ? 蓝色的高亮注释
          // * 绿色的高亮注释
          // todo 橙色的高亮注释
          // // 灰色带删除线的注释
        */
    v.图片
        a.语法
            ![1.png](../assets/1.png)
        b.位置
            <div align="right">                                                         --TY √   OB √
                <img src="../assets/1.png" />
            </div>
            <img src="../assets/1.png" align="right" />                                 --TY √   OB ×
        c.等比缩放
            a.相对父级元素(百分比,只定义宽,即可等比例缩放)
                <div align="right">                                                     --TY √   OB √
                    <img src="../assets/1.png" width="50%" />
                </div>
            b.相对自身(绝对数值,默认单位为px,只指定宽和高中的任意一个,即可等比例缩放)
                <div align="right">                                                     --TY √   OB √
                    <img src="../assets/1.png" width="240px" />
                </div>
            c.相对自身(大部分编辑器不支持CSS样式,如OB、GitHub不支持CSS样式)
                <div align="right">
                    <img src="../assets/1.png" style="zoom:50%;" />                     --TY √   OB ×
                </div>
        d.非等比缩放
            a.相对自身(绝对数值,默认单位为px,同时指定宽和高,即可非等比缩放)
                <div align="right">
                    <img src="../assets/1.png" width="384" height="140" />              --TY √   OB √
                </div>
            b.相对自身(大部分编辑器不支持CSS样式,如OB、GitHub不支持CSS样式)
                <img src="../assets/1.png"
                     style="transform:scale(0.8,0.5);transform-origin:left top;" />     --TY √   OB ×
        e.其他不常用的软件
            a.Mou、Marked 2
                ![](./pic/pic1_50.png =100x20)                                          --并非标准MD
                ![](./pic/pic1_50.png =100)                                             --并非标准MD
            b.URL末尾指定位置标识
                ![](./pic/pic1_50.png#pic_left)                                         --并非标准MD
            c.Orange
                ![[filename.png#position|caption|size]]                                 --Orange
                    position:left、center、right                                        --Orange
                    caption:图片说明                                                    --Orange
                    size:width * height,指定width,height自适应                        --Orange
        f.综述
            a.注意
                Typora:标准的MD语法编辑器
                OB、GitHub:无法使用标签内的 align 来改变【位置】、无法使用标签内的 CSS样式 来改变【缩放比例】
            b.统一标准
                默认本地链接,手动Upload进行联网备份;若需要更改本地链接,批量替换;
                关闭Image auto upload Plugin自动上传后,
                直接粘贴图片到OB中,默认为![](删除文件.assets/删除文件_20220207_011721.png);
                右键点击图片后,可以更改YYYY/MM/DD/快速新建文件夹,并上传图片到Github;切换boomb按照文件名排序;
                此时剪切板将变为自定义的链接格式;
                ------------------------------------------------------------------------------------------
                开启Image auto upload Plugin自动上传后,
                直接粘贴图片到OB中,默认为如下【第1种 + 第2种】,上传成功后,此时剪切板将变为自定义的链接格式;
                ![](https://cdn/./picgo/2022/02/06/20220206_232507.png)
                ![](当前文件名.assets/当前文件名_20220206_232506.png)
                <div align="center">
                    <img src="https://cdn/../picgo/2022/02/06/20220206_232507.png" width="500px"/>
                </div>
            c.Ob无法兼容HTML中使用本地图片
                <div align="center">
                    <img src="插件列表.assets/image.png" width="500px"/>                 --图片:显示错误
                </div>
                <div align="center">
                    <img src="https://cdn/../image.png" width="500px"/>                 --图片:显示正确
                </div>
                ![](插件列表.assets/image.png)                                           --图片:显示正确
                ![](https://cdn/../image.png)                                           --图片:显示正确
            d.URL参数中出现汉字、特殊字符(+、◡、/、?、%、#、&、=)
                +       +号代表空格                %2B
                ◡       空格可以用+号或者编码       %20
                /       分隔目录和子目录            %2F
                ?       分隔实际的URL和参数     %3F
                %       指定特殊字符               %25
                #       表示书签                   %23
                &       参数间的分隔符              %26
                =       参数的值                   %3D
                原因:汉字、特殊字符直接放在URL时,可能会引起解析程序的歧义,因此必须编码(服务器识别字符)才能使用
            e.使用建议
                <div>
                    <div style="float:left; width: 50%;">
                      <img src="插件列表.assets/image.png" />                            --图片:显示错误
                    </div>
                    <div style="float:left; width: 50%;">
                        <h3>高尔夫</h3>
                        <p>高尔夫球运动</p>
                    </div>
                </div>
                ----------------------------------------------------------------------------------------------
                <div>
                    <div style="float:left; width: 50%;">>
                      <img src="插件列表.assets/image.png" />                            --图片:显示错误
                    </div>
                    <div style="float:left; width: 50%;">>
                      <img src="插件列表.assets/image.png" />                            --图片:显示错误
                    </div>
                </div>
                ----------------------------------------------------------------------------------------------
                <a href="https://cdn.jsdelivr.net/gh/halavah/PicGo@master/background/23.jpg" target="_blank">
                   <img src="https://cdn.jsdelivr.net/gh/halavah/PicGo@master/background/23.jpg" />
                </a>
            f.链接示例
                https://github.com/halavah/blog/tree/master/xblog/
                https://github.com/halavah/blog/tree/master/xblog-tiny/
                https://github.com/halavah/blog/tree/master/xblog/doc/chapter01/Part01-博客页面划分.md
                ----------------------------------------------------------------------------------------------
                https://raw.githubusercontent.com/halavah/PinGo/master/avatar/02.jpg
                https://raw.githubusercontent.com/halavah/PinGo/master/background/22.jpg
                ----------------------------------------------------------------------------------------------
                https://cdn.jsdelivr.net/gh/halavah/PicGo/avatar/02.jpg
                https://cdn.jsdelivr.net/gh/halavah/PicGo/icon/01.ico
                https://cdn.jsdelivr.net/gh/halavah/PicGo/background/22.jpg
                https://cdn.jsdelivr.net/gh/halavah/PicGo/blog/menu_bg.jpg
                https://cdn.jsdelivr.net/gh/halavah/PicGo/blog/home_top_bg.jpg
                https://cdn.jsdelivr.net/gh/halavah/PicGo/sponsor/wechat.jpg
                https://cdn.jsdelivr.net/gh/halavah/PicGo/sponsor/alipay.jpg

04.术语文档
    a.目录
        └─vuepress
        │  About.md
        │  README.md
        │
        ├─.vuepress
        │  │  config.js
        │  │  enhanceApp.js
        │  │
        │  └─public
        │      └─logo
        │              favicon.ico
        │
        ├─Chapter01
        │      Part01-博客页面划分.md
        │      Part02-MyBatis-Plus的使用.md
        │      Part03-Controller控制层接口.md
        │      Part04-自定义Freemaker标签.md
        │      Part05-项目启动前加载导航栏.md
        │      Part06-侧边栏本周热议.md
        │      Part07-文章阅读缓存访问量.md
        │
        ├─Chapter02
        │      Part01-集成Kaptcha实现用户注册.md
        │      Part02-集成Shiro实现用户登录.md
        │      Part03-集成Shiro实现个人账户-我的主页、基本设置.md
        │      Part04-集成Shiro实现个人账户-用户中心.md
        │      Part05-集成Shiro实现个人账户-我的消息.md
        │
        ├─Chapter03
        │      Part01-集成Shiro实现博客详情-收藏文章.md
        │      Part02-集成Shiro实现博客详情-添加文章、编辑文章、提交文章.md
        │      Part03-集成Shiro实现博客详情-超级用户、删除、置顶、精华.md
        │      Part04-集成Shiro实现博客详情-用户文章、用户评论.md
        │
        ├─Chapter04
        │      Part01-集成WeSocket实现用户评论-即时通讯.md
        │      Part02-集成Elasticsearch实现文章内容-搜索引擎.md
        │      Part03-集成RabbitMQ保证ES随文章增删改查-实时更新.md
        │      Part04-集成WebSocket-tio实现网络群聊-聊天室.md
        │
        └─command
                build.bat
                deploy.bat
                dev.bat
    b.配置
        .vuepress/config.js -> base: '<repo>',
    c.启动
        build.bat
        deploy.bat
        dev.bat

1.3 在线API文档

01.Swagger2
    a.注解
        a.Entity
            a.@ApiModel             用于Entity类,类名(标识)
                value       String      模型描述
                description String      模型说明
            b.@ApiModelProperty     用于Entity类,属性(描述)
                name        String      属性名称
                value       String      属性描述
                dataType    Stirng      属性类型
                required    boolean     参数必填,默认false
        b.Controller——类
            a.@Api                  用于Controller类,类名(标识)
                value   String          类的描述
                tags    String[]        类的描述,覆盖value值
            b.@ApiIgnore            用于Controller类,类名、属性、方法(忽略标识)
                无
        c.Controller——方法
            a.@ApiOperation         用于Controller类,方法(描述)
                value   String          方法描述
                notes   String          方法备注
            b.@ApiParam             用于Controller类,方法(参数:参数列表)
                name        String      参数名称
                value       String      参数描述
                required    boolean     参数必填,默认false
                defaultValueString      参数默认
            c.@ApiImplicitParam     用于Controller类,方法(参数:单个参数)
                name        String      参数名称
                value       String      参数描述
                required    boolean     参数必填,默认false
                defaultValueString      参数默认
                paramType   String      参数位置,@RequestHeader、@RequestParam、@PathVariable、body、form
                dataType    String      参数类型,默认String,dataType=“Integer”
            d.@ApiImplicitParams    用于Controller类,方法(参数:多个参数)
                无
        d.Controller——响应
            a.@ApiError             用于Controller类,方法(响应:接口信息)
                无
            b.@ApiResponse          用于Controller类,方法(响应:单个信息)
                code        int         响应状态码
                message     String      响应的信息
                response    Class<?>    抛出异常类
            c.@ApiResponses         用于Controller类,方法(响应:多个信息)
                无
        e.Controller——常用
            a.@PathVariable:获取get请求url路径上的参数,参数绑定的作用,即url中"?"前面绑定的参数
                @GetMapping("/query/{id}")
                private List<Student> queryById (@PathVariable("id") Long id) {
                    List<Student> studentList = studentService.queryById(id);
                    return studentList;
                }
            b.@RequestParam:用于获取前端传过来的参数,可以是get、post请求,即url中"?"后面拼接的每个参数
                @GetMapping("/query/student")
                private List<Student> queryByIdStu( @RequestParam("id") Long id) {
                    List<Student> studentList = studentService.queryById(id);
                    return studentList;
                }
        f.汇总
            | @Api              | 用在类上,例如Controller,表示对类的说明
            |-------------------|----------------------------------
            | @ApiModel         | 用在类上,例如entity、DTO、VO
            | @ApiModelProperty | 用在属性上,描述属性信息
            | @ApiOperation     | 用在方法上,例如Controller的方法,说明方法的用途、作用
    b.示例
        a.Entity
            @ApiModel(value = "用户", description = "查询用户信息")
            public class User {
                @ApiModelProperty(value = "用户id", required=true)
                private Integer id;
            }
        b.Controller
            @Api(tags = "用户数据接口")
            @RestController
            public class UserController {
                @ApiOperation(value = "查询全部用户")
                @GetMapping("/user/list")
                public List<User> list() {
                    return list;
                }

                @ApiOperation(value = "查询分页用户")
                @GetMapping("/user/page")
                @ApiImplicitParams({
                        @ApiImplicitParam(name = "pageNum", value = "当前页数"),
                        @ApiImplicitParam(name = "pageSize", value = "每页记录数")
                })
                public List<User> page(
                        @RequestParam(defaultValue = "1", required = false) Integer pageNum,
                        @RequestParam(defaultValue = "10", required = false) Integer pageSize) {
                    return list;
                }

                @ApiOperation(value = "查询某个用户", notes = "根据用户id查询用户")
                @ApiImplicitParam(name = "id", value = "用户id", required = true, defaultValue = "99")
                @GetMapping("/user/{userId}")
                public User getUserById((@PathVariable("userId") Integer id) {
                    for (User user : list) {
                        if (user.getUserId().equals(userId)) {
                            return user;
                        }
                    }
                    return user;
                }

                @ApiOperation(value = "添加某个用户")
                @PostMapping("/user")
                public Boolean insert(@RequestBody @ApiParam(name="User",value="用户",required=true)Useruser){
                    User user = new User(1, "xxx");
                    list.add(user);
                    return true;
                }

                @ApiOperation(value = "删除某个用户", notes = "根据用户id删除用户")
                @ApiImplicitParam(name = "id", value = "用户id", required = true, defaultValue = "99")
                @ApiResponses({
                    @ApiResponse(code = 200, message = "删除成功"),
                    @ApiResponse(code = 500, message = "删除失败")
                })
                @DeleteMapping("/user/{id}")
                public void deleteUserById(@PathVariable("id") Integer id) {
                     Iterator<User> iterator = list.iterator();
                    while (iterator.hasNext()) {
                        if (iterator.next().getUserId().equals(userId)) {
                            iterator.remove();
                            return true;
                        }
                    }
                    return false;
                }

                @ApiOperation(value = "更新某个用户", notes = "根据用户id更新用户名")
                @ApiImplicitParams({
                    @ApiImplicitParam(name = "id",value = "用户id",required = true,defaultValue = "99"),
                    @ApiImplicitParam(name = "username",value = "用户名",required = true,defaultValue = "xxx")
                })
                @ApiResponses({
                    @ApiResponse(code = 200, message = "更新成功"),
                    @ApiResponse(code = 500, message = "更新失败")
                })
                @PutMapping("/user")
                public User updateUsernameById(String username, Integer id) {
                    User user = new User();
                    user.setId(id);
                    user.setUsername(username);
                    return user;
                }
            }

02.三种使用方式
    a.方式1:Swagger-ui
        a.依赖
            <dependencies>
                <dependency>
                    <groupId>io.springfox</groupId>
                    <artifactId>springfox-swagger2</artifactId>
                    <version>2.9.2</version>
                </dependency>
                <dependency>
                    <groupId>io.springfox</groupId>
                    <artifactId>springfox-swagger-ui</artifactId>
                    <version>2.9.2</version>
                </dependency>
            </dependencies>
        b.配置
            @EnableSwagger2
            @Configuration
            public class Swagger2Config {
                @Bean
                Docket docket() {
                    return new Docket(DocumentationType.SWAGGER_2)
                        .select()
                        .apis(RequestHandlerSelectors.basePackage("org.myslayers.swagger.controller"))
                        .paths(PathSelectors.any())
                        .build().apiInfo(new ApiInfoBuilder()
                            .title("接口文档的标题")
                            .description("接口文档的描述信息")
                            .version("v1.0")
                            .license("Apache2.0")
                            .build());
                }
            }
        c.启动
            https://editor.swagger.io/                                              --editor
            http://localhost:8080/v2/api-docs                                       --swagger.json
            http://localhost:8080/swagger-ui.html                                   --Swagger-ui
            http://localhost:8080/doc.html                                          --Swagger-bootstrap-ui
            http://localhost:8080/doc.html                                          --Knife4j
    b.方式2:Knife4j
        a.依赖
            <!-- Spring Boot单服务架构使用最新版的knife4j依赖,已经继承swagger依赖,同时增强UI实现 -->
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
                <version>2.0.7</version>
            </dependency>
        b.配置
            @Configuration
            @EnableSwagger2WebMvc
            public class Knife4jConfig {
                @Bean(value = "defaultApi2")
                public Docket defaultApi2() {
                    Docket docket=new Docket(DocumentationType.SWAGGER_2)
                        .apiInfo(new ApiInfoBuilder()
                            .title("文档标题")
                            .description("文档描述")
                            .termsOfServiceUrl("https://github.com/halavah")
                            .version("1.0")
                            .build())
                        //分组名称
                        .groupName("1.0")
                        .select()
                        //这里指定Controller扫描包路径
                        .apis(RequestHandlerSelectors.basePackage("org.myslayers.controller"))
                        .paths(PathSelectors.any())
                        .build();
                    return docket;
                }
            }
        c.启动
            https://editor.swagger.io/                                              --editor
            http://localhost:8080/v2/api-docs                                       --swagger.json
            http://localhost:8080/swagger-ui.html                                   --Swagger-ui
            http://localhost:8080/doc.html                                          --Swagger-bootstrap-ui
            http://localhost:8080/doc.html                                          --Knife4j
    c.方式3:screw
        a.依赖
            <!--数据库文档核心依赖-->
            <dependency>
              <groupId>cn.smallbun.screw</groupId>
              <artifactId>screw-core</artifactId>
              <version>1.0.2</version>
            </dependency>
            <!-- HikariCP -->
            <dependency>
              <groupId>com.zaxxer</groupId>
              <artifactId>HikariCP</artifactId>
              <version>3.4.5</version>
            </dependency>
            <!--mysql driver-->
            <dependency>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
              <version>8.0.20</version>
            </dependency>
        b.通过插件的形式生成文档
            <build>
                <plugins>
                    <plugin>
                        <groupId>cn.smallbun.screw</groupId>
                        <artifactId>screw-maven-plugin</artifactId>
                        <version>1.0.2</version>
                        <dependencies>
                            <!-- HikariCP -->
                            <dependency>
                                <groupId>com.zaxxer</groupId>
                                <artifactId>HikariCP</artifactId>
                                <version>3.4.5</version>
                            </dependency>
                            <!--mysql driver-->
                            <dependency>
                                <groupId>mysql</groupId>
                                <artifactId>mysql-connector-java</artifactId>
                                <version>8.0.20</version>
                            </dependency>
                        </dependencies>
                        <configuration>
                            <!--username-->
                            <username>root</username>
                            <!--password-->
                            <password>root</password>
                            <!--driver-->
                            <driverClassName>com.mysql.cj.jdbc.Driver</driverClassName>
                            <!--jdbc url-->
                            <jdbcUrl>jdbc:mysql://127.0.0.1:3306/vue_admin?serverTimezone=GMT%2B8</jdbcUrl>
                            <!--生成文件类型-->
                            <fileType>HTML</fileType>
                            <!--打开文件输出目录-->
                            <openOutputDir>true</openOutputDir>
                            <!--生成模板-->
                            <produceType>freemarker</produceType>
                            <!--描述-->
                            <description>数据库文档生成</description>
                            <!--版本-->
                            <version>${project.version}</version>
                            <!--标题-->
                            <title>数据库文档</title>
                        </configuration>
                        <executions>
                            <execution>
                                <phase>compile</phase>
                                <goals>
                                    <goal>run</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>

03.JApiDocs
    a.注解
        @param:接口方法,通过寻找接口参数和进一步解析参数内容
        @ApiDoc:接口方法,@ApiDoc(result = AdminVO.class, url = "/api/v1/admin/login2", method = "post")
        @Ignore:忽略接口、忽略方法、忽略字段
        @description:接口、接口方法,默认类注释会对应到一级接口分组,可以通过该注释来指定分组名称
    b.示例
        a.Entity
            public class UserListForm {
                private Integer status; //用户状态
                private String name; //用户名
            }
        b.Controller
            /**
             * 用户接口
             */
            @RequestMapping("/api/user/")
            @RestController
            public class UserController {
                /**
                 * 用户列表
                 * @param listForm
                 */
                @RequestMapping(path = "list", method = {RequestMethod.GET,  RequestMethod.POST}  )
                public ApiResult<PageResult<UserVO>> list(UserListForm listForm){
                    return null;
                }

                /**
                 * 保存用户
                 * @param userForm
                 */
                @PostMapping(path = "save")
                public ApiResult<UserVO> saveUser(@RequestBody UserForm userForm){
                    return null;
                }

                /**
                 * 删除用户
                 * @param userId 用户ID
                 */
                @PostMapping("delete")
                public ApiResult deleteUser(@RequestParam Long userId){
                    return null;
                }
            }
    c.JApiDocs
        a.依赖
            <dependency>
              <groupId>io.github.yedaxia</groupId>
              <artifactId>japidocs</artifactId>
              <version>1.4.4</version>
            </dependency>
        b.配置
            @SpringBootApplication
            public class SwaggerApplication {
                public static void main(String[] args) {
                    SpringApplication.run(SwaggerApplication.class, args);
                    DocsConfig config = new DocsConfig();
                    config.setProjectPath("D:\\");                          // 项目根目录
                    config.setProjectName("ProjectName");                   // 项目名称
                    config.setApiVersion("V1.0");                           // 声明该API的版本
                    config.setDocsPath("D:\\");                             // 生成API 文档所在目录
                    config.setAutoGenerate(Boolean.TRUE);                   // 配置自动生成
                    Docs.buildHtmlDocs(config);                             // 执行生成文档
                }
            }
        c.启动
            任意main方法运行代码生成

2 Chapter02

2.1 前端风格

01.Rules
    a.模块命名
        标志:logo
        图标:icon
        头部:header
        导航:nav
        底部:footer
        搜索:search
        新闻:news
    b.CSS样式顺序
        位置属性:position、top、right、z-index、display、float
        尺寸大小:width、height、padding、margin
        背景边框:background、border
        文字系列:font、line-height、letter-spacing、color- text-align
        其他:animation、transition
    c.CSS样式风格
        驼峰式: solutionTitle、solutionDetail
        中横杠: solution-title、solution-detail
        下划线: solution_title、solution_detail

02.Emmet
    a.基础语法
        E                                                                   --HTML标签
        E#id                                                                --id属性
        E.class                                                             --class属性
        E[attr=foo]                                                         --某一个特定属性
        E{foo}                                                              --标签包含的内容是foo
        E>N                                                                 --N是E的子元素
        E+N                                                                 --N是E的同级元素
        E^N                                                                 --N是E的上级元素
    b.常见标签
        a.HTML标签
            <!DOCTYPE html>...</html>                                       --!
            <a href=""></a>                                                 --a
            <div></div>                                                     --div
            <img src="" alt="">                                             --img
            <link rel="stylesheet" href="" />                               --link
            <script src=""></script>                                        --script:src
        b.id属性、class属性(默认标签为div)
            <div id="abc"></div>                                            --#abc
            <div class="abc"></div>                                         --.abc
            <div class="abc def ghi"></div>                                 --.abc.def.ghi
            <div id="abc"></div>                                            --div#abc
            <div class="abc"></div>                                         --div.abc
        c.input
            <input type="url" name="" id="">                                --input:url
            <input type="file" name="" id="">                               --input:file
            <input type="reset" name="" id="">                              --input:reset
            <input type="button" value="">                                  --input:button
            <input type="submit" value="">                                  --input:submit
            <input type="checkbox" name="" id="">                           --input:checkbox
            <input type="password" name="" id="">                           --input:password
        d.form
            <form action=""></form>                                         --form:
            <form action="" method="get"></form>                            --form:get
            <form action="" method="post"></form>                           --form:post
        e.后代元素、同级元素、上级元素
            <nav><ul><li></li></ul></nav>                                   --nav>ul>li
            <div></div><p></p><blockquote></blockquote>                     --div+p+bq
            <ul><li></li><li></li><li></li><li></li><li></li></ul>          --ul>li*5

2.2 [重]概念1

00.概念
    DB_BORROW
    id   uid  bid
    1    1    1
    2    1    2
    3    2    2
    4    2    3
    5    3    2
    6    3    4
    7    2    1
    ---------------------------------------------------------------------------------------------------------
    @Mapper
    public interface BorrowMapper {
        @Select("select * from DB_BORROW where uid = #{uid}")
        List<Borrow> getBorrowsByUid(int uid);

        @Select("select * from DB_BORROW where bid = #{bid}")
        List<Borrow> getBorrowsByBid(int bid);

        @Select("select * from DB_BORROW where bid = #{bid} and uid = #{uid}")
        Borrow getBorrow(int uid, int bid);
    }
    ---------------------------------------------------------------------------------------------------------
    @Service
    public class BorrowServiceImpl implements BorrowService {

        @Resource
        BorrowMapper mapper;

        @Override
        public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
            List<Borrow> borrow = mapper.getBorrowsByUid(uid);
            //RestTemplate支持多种方式的远程调用
            RestTemplate template = new RestTemplate();
            //这里通过调用getForObject来请求其他服务,并将结果自动进行封装
            //获取User信息
            User user = template.getForObject("http://localhost:8101/user/"+uid, User.class);
            //获取每一本书的详细信息
            List<Book> bookList = borrow
                    .stream()
                    .map(b -> template.getForObject("http://localhost:8201/book/"+b.getBid(), Book.class))
                    .collect(Collectors.toList());
            return new UserBorrowDetail(user, bookList);
        }
    }
    ---------------------------------------------------------------------------------------------------------
    {                                                            --数组对象
        "user": {                                                --对象
            "uid": 1,
            "name": "小明",
            "sex": "男"
        },
        "bookList": [                                            --对象数组
            {                                                    --对象
                "bid": 1,
                "title": "深入了解Java虚拟机",
                "desc": "了解Java的底层运作机制"
            },
            {
                "bid": 2,
                "title": "Java并发编程的艺术",
                "desc": "了解并发编程的高级玩法"
            }
        ]
    }

00.概念
    数组[]         array          [1, 2, 3, 4, 5]
    对象{}         object         {name: "John", age: 25}
    对象数组       本质是数组      [{ name: "John", age: 25 }, { name: "Jane", age: 30 }]
    数组对象       本质是对象      {numbers: [1, 2, 3, 4, 5], names: ["Alice", "Bob", "Charlie"]};
    ---------------------------------------------------------------------------------------------------------
    行row
    列colum
    占位符{{}}

01.数组(Array)
    数组是一个包含相同类型元素的有序集合。
    在Java中,数组的大小在创建时确定,并且不能更改。
    在JavaScript中,数组是动态的,可以根据需要增加或减少元素。
    ---------------------------------------------------------------------------------------------------------
    数组是一种数据结构,用于存储相同类型的多个元素。它们是一维的,可以通过索引访问和操作元素。
    ---------------------------------------------------------------------------------------------------------
    示例(Java):
        int[] numbers = new int[5]; // 创建一个包含5个整数的数组
        numbers[0] = 1; // 在索引0处设置值
        numbers[1] = 2;
        System.out.println(numbers[0]); // 输出: 1
    ---------------------------------------------------------------------------------------------------------
    示例(JavaScript):
        let numbers = [1, 2, 3, 4, 5]; // 创建一个包含5个数字的数组
        numbers[0] = 10; // 在索引0处设置新值
        console.log(numbers[0]); // 输出: 10
    ---------------------------------------------------------------------------------------------------------
    示例(Java):
        int[] numbers = {1, 2, 3, 4, 5};
        String[] names = {"Alice", "Bob", "Charlie"};
    ---------------------------------------------------------------------------------------------------------
    示例(JavaScript):
        let numbers = [1, 2, 3, 4, 5];
        let names = ["Alice", "Bob", "Charlie"];

02.对象(Object)
    对象是一种复合数据类型,它可以包含多个属性和对应的值。
    在Java中,对象是基于类定义的,具有特定的属性和方法。
    在JavaScript中,对象是键值对的集合,属性和值可以是任何类型。
    ---------------------------------------------------------------------------------------------------------
    对象是一种复合数据类型,用于存储多个相关属性和方法。每个属性都由键值对(key-value pair)表示,其中键是字符串,值可以是任何数据类型。
    ---------------------------------------------------------------------------------------------------------
    示例(Java):
        class Person {
            String name;
            int age;
        }

        Person person = new Person();
        person.name = "John";
        person.age = 25;
        System.out.println(person.name); // 输出: John
    ---------------------------------------------------------------------------------------------------------
    示例(JavaScript):
        let person = {
            name: "John",
            age: 25
        };
        console.log(person.name); // 输出: John
    ---------------------------------------------------------------------------------------------------------
    示例(Java):
        class Person {
          String name;
          int age;
        }

        Person person = new Person();
        person.name = "Alice";
        person.age = 25;
    ---------------------------------------------------------------------------------------------------------
    示例(JavaScript):
        let person = {
          name: "Alice",
          age: 25
        };

03.对象数组(Array of Objects)
    对象数组是一个数组,其中每个元素都是一个对象。
    在Java和JavaScript中,对象数组都可以包含具有相同类型的对象。
    ---------------------------------------------------------------------------------------------------------
    对象数组是指包含多个对象的数组。数组的每个元素都是一个对象,可以具有相同或不同的属性。
    ---------------------------------------------------------------------------------------------------------
    示例(Java):
        class Person {
            String name;
            int age;
        }

        Person[] people = new Person[2];

        Person person1 = new Person();
        person1.name = "John";
        person1.age = 25;

        Person person2 = new Person();
        person2.name = "Jane";
        person2.age = 30;

        people[0] = person1;
        people[1] = person2;

        System.out.println(people[0].name); // 输出: John
        System.out.println(people[1].age); // 输出: 30
    ---------------------------------------------------------------------------------------------------------
    示例(JavaScript):
        let people = [
            { name: "John", age: 25 },
            { name: "Jane", age: 30 }
        ];
        console.log(people[0].name); // 输出: John
        console.log(people[1].age); // 输出: 30
    ---------------------------------------------------------------------------------------------------------
    示例(Java):
        class Person {
          String name;
          int age;
        }

        Person[] people = new Person[2];
        people[0] = new Person();
        people[0].name = "Alice";
        people[0].age = 25;

        people[1] = new Person();
        people[1].name = "Bob";
        people[1].age = 30;
    ---------------------------------------------------------------------------------------------------------
    示例(JavaScript):
        let people = [
          { name: "Alice", age: 25 },
          { name: "Bob", age: 30 }
        ];

04.数组对象(Object of Arrays):
    数组对象是一个对象,其属性值是数组。
    在Java和JavaScript中,数组对象都可以将数组作为属性值。
    ---------------------------------------------------------------------------------------------------------
    数组对象是指包含多个数组的对象。对象的每个属性都是一个数组,可以具有相同或不同的长度。
    ---------------------------------------------------------------------------------------------------------
    示例(Java):
        class Person {
            String[] hobbies;
        }

        Person person = new Person();
        person.hobbies = new String[]{"reading", "running", "cooking"};

        System.out.println(person.hobbies[0]); // 输出: reading
        System.out.println(person.hobbies.length); // 输出: 3
    ---------------------------------------------------------------------------------------------------------
    示例(JavaScript):
        let person = {
            hobbies: ["reading", "running", "cooking"]
        };
        console.log(person.hobbies[0]); // 输出: reading
        console.log(person.hobbies.length); // 输出: 3
    ---------------------------------------------------------------------------------------------------------
    示例(Java):
        import java.util.Arrays;

        class Data {
          int[] numbers;
          String[] names;
        }

        Data data = new Data();
        data.numbers = new int[]{1, 2, 3, 4, 5};
        data.names = new String[]{"Alice", "Bob", "Charlie"};

        System.out.println(Arrays.toString(data.numbers));
        System.out.println(Arrays.toString(data.names));
    ---------------------------------------------------------------------------------------------------------
    示例(JavaScript):
        let data = {
          numbers: [1, 2, 3, 4, 5],
          names: ["Alice", "Bob", "Charlie"]
        };

        console.log(data.numbers);
        console.log(data.names);

2.3 [重]概念2

00.概念
    列表List                      ["Alice", "Bob", "Charlie"];
    集合Set                       ["Alice", "Bob", "Charlie"];
    ---------------------------------------------------------------------------------------------------------
    映射Map                       {'Alice':25, 'Bob':30, 'Charlie':35}

01.List(列表)
    List是一种有序的集合,允许重复元素。它可以根据索引访问元素,还提供了丰富的方法用于添加、删除和操作元素。
    ---------------------------------------------------------------------------------------------------------
    Java中的示例:
        import java.util.ArrayList;
        import java.util.List;

        List<String> names = new ArrayList<>();
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");

        System.out.println(names);
    ---------------------------------------------------------------------------------------------------------
    JavaScript中的示例:
        let names = ["Alice", "Bob", "Charlie"];
        console.log(names);

02.Set(集合)
    Set是一种无序的集合,不允许重复元素。它提供了一些方法用于添加、删除和检查元素的存在性。
    ---------------------------------------------------------------------------------------------------------
    Java中的示例:
        import java.util.HashSet;
        import java.util.Set;

        Set<String> names = new HashSet<>();
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");

        System.out.println(names);
    ---------------------------------------------------------------------------------------------------------
    JavaScript中的示例:
        let names = new Set();
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");
        console.log(names);

03.Map(映射)
    Map是一种键值对的集合,其中每个键都是唯一的。它提供了一些方法用于添加、删除和根据键访问值。
    ---------------------------------------------------------------------------------------------------------
    Java中的示例:
        import java.util.HashMap;
        import java.util.Map;

        Map<String, Integer> ages = new HashMap<>();
        ages.put("Alice", 25);
        ages.put("Bob", 30);
        ages.put("Charlie", 35);

        System.out.println(ages);
    ---------------------------------------------------------------------------------------------------------
    JavaScript中的示例:
        let ages = new Map();
        ages.set("Alice", 25);
        ages.set("Bob", 30);
        ages.set("Charlie", 35);
        console.log(ages);
        -----------------------------------------------------------------------------------------------------
         {'Alice' => 25, 'Bob' => 30, 'Charlie' => 35}

3 Chapter03

3.1 tomcat:ruoyi

00.区别
    spring boot既可以打成war发布,也可以找成jar包发布。
    1、jar部署方式:使用命令行执行:java –jar ruoyi.jar 或者执行脚本:ruoyi/bin/run.bat
    2、war部署方式:ruoyi/pom.xml中的packaging修改为war,放入tomcat服务器webapps
    ---------------------------------------------------------------------------------------------------------
    jar包:直接通过内置tomcat运行,不需要额外安装tomcat。如需修改内置tomcat的配置,只需要在spring boot的配置文件中配置。
    内置tomcat没有自己的日志输出,全靠jar包应用输出日志。但是比较方便,快速,比较简单。
    ---------------------------------------------------------------------------------------------------------
    war包:传统的应用交付方式,需要安装tomcat,然后放到waeapps目录下运行war包,可以灵活选择tomcat版本,
    可以直接修改tomcat的配置,有自己的tomcat日志输出,可以灵活配置安全策略。相对打成jar包来说没那么快速方便。

01.war包
    a.修改pom.xml为war包
        ruoyi-admin/pom.xml:13
        -----------------------------------------------------------------------------------------------------
        <parent>
            <artifactId>ruoyi</artifactId>
            <groupId>com.ruoyi</groupId>
            <version>4.7.5</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <packaging>war</packaging>                                                --war包
        <artifactId>ruoyi-admin</artifactId>
    b.打包
        mvn package
    c.将【ruoyi-admin.war】放到【...\apache-tomcat-9.0.34\webapps】
        C:\software\apache-tomcat-9.0.34\webapps
        │  ruoyi-admin.war                                                        --以war包放在此处
        ├─docs
        ├─examples
        ├─host-manager
        ├─manager
        ├─ROOT
    d.启动tomcat
        ...\apache-tomcat-9.0.34\bin\startup.bat
        ...\apache-tomcat-9.0.34\bin\startup.sh
    e.访问
        http://127.0.0.1:8080/ruoyi-admin/                                        --8080为tomcat端口,路径必须带ruoyi-admin(war名)
    f.war包,可以去除内置tomcat,然后使用外置tomcat启动
        <!-- 多模块排除内置tomcat -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        ---------------------------------------------------------------------------------------------------------
        <!-- 单应用排除内置tomcat -->
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter-tomcat</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>

02.jar包
    a.修改pom.xml为jar包
        ruoyi-admin/pom.xml:13
        -----------------------------------------------------------------------------------------------------
        <parent>
            <artifactId>ruoyi</artifactId>
            <groupId>com.ruoyi</groupId>
            <version>4.7.5</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <packaging>jar</packaging>                                                --jar包
        <artifactId>ruoyi-admin</artifactId>
    b.打包
        mvn package
    c.启动
        java -jar ruoyi-admin.jar
    d.访问
        http://127.0.0.1:80/

3.2 baota:ruoyi-vue

00.端口
    a.查看8080端口
        [root@bigdata01 ~]# netstat -lnpt | grep 8080
        tcp6       0      0 :::8080                 :::*                    LISTEN      3201/java
    b.关闭8080端口
        [root@bigdata01 ~]# kill -9 3201

01.SpringBoot中jar运行方式
    a.目录
        cd /data/ruoyi/
    b.第1种
        java -jar ruoyi-admin.jar
    c.第2种
        nohup java -jar ruoyi-admin.jar > log.file 2>&1 &
    d.Java项目一键部署
        sudo -u springboot nohup /usr/java/jdk1.8.0_121/bin/java -jar /data/ruoyi/ruoyi-admin.jar  >> /tmp/L90D605NMI.log 2>&1 &
    e.ruoyi自定义脚本
        chmod -R 777 ry.sh
        ./ry.sh start
        ./ry.sh stop
        ./ry.sh staus
        ./ry.sh restart

02.打包
    a.后端
        mvn package
    b.前端
        npm run build:prod

03.nginx配置
    a.全部
        user  www www;
        worker_processes auto;
        error_log  /www/wwwlogs/nginx_error.log  crit;
        pid        /www/server/nginx/logs/nginx.pid;
        worker_rlimit_nofile 51200;

        stream {
            log_format tcp_format '$time_local|$remote_addr|$protocol|$status|$bytes_sent|$bytes_received|$session_time|$upstream_addr|$upstream_bytes_sent|$upstream_bytes_received|$upstream_connect_time';
            access_log /www/wwwlogs/tcp-access.log tcp_format;
            error_log /www/wwwlogs/tcp-error.log;
            include /www/server/panel/vhost/nginx/tcp/*.conf;
        }

        events {
            use epoll;
            worker_connections 51200;
            multi_accept on;
        }

        http{
            include       mime.types;
            #include luawaf.conf;

            include proxy.conf;

            default_type  application/octet-stream;

            server_names_hash_bucket_size 512;
            client_header_buffer_size 32k;
            large_client_header_buffers 4 32k;
            client_max_body_size 50m;

            sendfile   on;
            tcp_nopush on;

            keepalive_timeout 60;

            tcp_nodelay on;

            fastcgi_connect_timeout 300;
            fastcgi_send_timeout 300;
            fastcgi_read_timeout 300;
            fastcgi_buffer_size 64k;
            fastcgi_buffers 4 64k;
            fastcgi_busy_buffers_size 128k;
            fastcgi_temp_file_write_size 256k;
            fastcgi_intercept_errors on;

            gzip on;
            gzip_min_length  1k;
            gzip_buffers     4 16k;
            gzip_http_version 1.1;
            gzip_comp_level 2;
            gzip_types     text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
            gzip_vary on;
            gzip_proxied   expired no-cache no-store private auth;
            gzip_disable   "MSIE [1-6]\.";

            limit_conn_zone $binary_remote_addr zone=perip:10m;
            limit_conn_zone $server_name zone=perserver:10m;

            server_tokens off;
            access_log off;

            server {
                listen 888;
                server_name phpmyadmin;
                index index.html index.htm index.php;
                root  /www/server/phpmyadmin;
                    location ~ /tmp/ {
                        return 403;
                    }

                #error_page   404   /404.html;
                include enable-php.conf;

                location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
                {
                    expires      30d;
                }

                location ~ .*\.(js|css)?$
                {
                    expires      12h;
                }

                location ~ /\.
                {
                    deny all;
                }

                access_log  /www/wwwlogs/access.log;
            }

            server {
                listen       80;
                server_name  localhost;
                charset utf-8;

                location / {
                    root   /data/ruoyi/dist;
                    try_files $uri $uri/ /index.html;
                    index  index.html index.htm;
                }

                location /prod-api/ {
                    proxy_set_header Host $http_host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header REMOTE-HOST $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_pass http://localhost:8080/;
                }

                error_page   500 502 503 504  /50x.html;
                location = /50x.html {
                    root   html;
                }
            }

            include /www/server/panel/vhost/nginx/*.conf;
        }
    b.ruoyi
            server {
                listen       80;                                                       # 前端的端口
                server_name  localhost;                                                # Linux的ip地址,不建议用localhost
                charset utf-8;

                location / {
                    root   /data/ruoyi/dist;                                           # 前端的包所在路径
                    try_files $uri $uri/ /index.html;                                  # 按此顺序查找请求的文件
                    index  index.html index.htm;
                }

                location /prod-api/ {                                                  # 生产环境的请求都是以/prod-api,可以按F12随便找一个请求看看它的路径
                    proxy_set_header Host $http_host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header REMOTE-HOST $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_pass http://localhost:8080/;                                 # 转发到后端
                }

                error_page   500 502 503 504  /50x.html;
                location = /50x.html {
                    root   html;
                }
            }
    c.解读
        1、全局块:配置影响nginx全局的指令。一般有运行nginx服务器的用户组,nginx进程pid存放路径,日志存放路径,配置文件引入,允许生成worker process数等。
        2、events块:配置影响nginx服务器或与用户的网络连接。有每个进程的最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网路连接,开启多个网络连接序列化等。
        3、http块:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。如文件引入,mime-type定义,日志自定义,是否使用sendfile传输文件,连接超时时间,单连接请求数等。
        4、server块:配置虚拟主机的相关参数,一个http中可以有多个server。
        5、location块:配置请求的路由,以及各种页面的处理情况。

3.3 docker:ruoyi

01.目录
    ...\集成DOCKER实现一键部署
    │  docker-compose.yml
    │  mysql-dockerfile
    │  ruoyi-dockerfile
    │
    ├─db
    |
    └─jar

02.说明
    a.docker-compose.yml
        version : '3'
        services:
          ruoyi-mysql:
            container_name: ruoyi-mysql
            image: mysql:5.7
            build:
              context: .
              dockerfile: mysql-dockerfile
            ports:
              - "3306:3306"
            volumes:
              - ./mysql/conf:/etc/mysql/conf.d
              - ./mysql/logs:/logs
              - ./mysql/data:/var/lib/mysql
            command: [
                  'mysqld',
                  '--innodb-buffer-pool-size=80M',
                  '--character-set-server=utf8mb4',
                  '--collation-server=utf8mb4_unicode_ci',
                  '--default-time-zone=+8:00',
                  '--lower-case-table-names=1'
                ]
            environment:
              MYSQL_DATABASE: ry
              MYSQL_ROOT_PASSWORD: password
          ruoyi-server:
            container_name: ruoyi-server
            build:
              context: .
              dockerfile: ruoyi-dockerfile
            ports:
              - "80:80"
            volumes:
              - ./ruoyi/logs:/home/ruoyi/logs
              - ./ruoyi/uploadPath:/home/ruoyi/uploadPath
            depends_on:
              - ruoyi-mysql
            links:
              - ruoyi-mysql
    b.mysql-dockerfile
        # 基础镜像
        FROM mysql:5.7
        # author
        MAINTAINER ruoyi

        # 执行sql脚本
        ADD ./db/*.sql /docker-entrypoint-initdb.d/
    c.ruoyi-dockerfile
        # 基础镜像
        FROM java:8
        # author
        MAINTAINER ruoyi

        # 挂载目录
        VOLUME /home/ruoyi
        # 创建目录
        RUN mkdir -p /home/ruoyi
        # 指定路径
        WORKDIR /home/ruoyi
        # 复制jar文件到路径
        COPY ./jar/*.jar /home/ruoyi/ruoyi.jar
        # 启动应用
        ENTRYPOINT ["java","-jar","ruoyi.jar"]

03.修改配置
    a.application.yml
        # 项目相关配置
        ruoyi:
          # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
          profile: /home/ruoyi/uploadPath
    b.application-druid.yml
        spring:
            datasource:
                type: com.alibaba.druid.pool.DruidDataSource
                driverClassName: com.mysql.cj.jdbc.Driver
                druid:
                    # 主库数据源
                    master:
                        url: jdbc:mysql://ruoyi-mysql:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                        username: root
                        password: password

04.docker操作
    a.启动docker
        [root@bigdata03 ~]# systemctl start docker
    b.构建docker服务
        [root@bigdata03 ~]# cd /home/docker/
        -----------------------------------------------------------------------------------------------------
        [root@bigdata03 docker]# ll
        total 12
        drwxr-xr-x 2 root root  47 Jul 28 21:11 db
        -rw-r--r-- 1 root root 984 Jul 28 21:11 docker-compose.yml
        drwxr-xr-x 2 root root  29 Jul 28 21:11 jar
        drwxr-xr-x 5 root root  42 Jul 28 21:14 mysql
        -rw-r--r-- 1 root root 126 Jul 28 21:11 mysql-dockerfile
        drwxr-xr-x 4 root root  36 Jul 28 21:14 ruoyi
        -rw-r--r-- 1 root root 296 Jul 28 21:11 ruoyi-dockerfile
        -----------------------------------------------------------------------------------------------------
        [root@bigdata03 docker]# docker-compose build
        Building ruoyi-mysql
        Step 1/3 : FROM mysql:5.7
    c.启动docker容器
        [root@bigdata03 docker]# docker-compose up -d
        Creating network "docker_default" with the default driver
        Creating ruoyi-mysql ... done
        Creating ruoyi-redis ... done
        Creating ruoyi-server ... done
        Creating ruoyi-nginx  ... done
    d.访问
        http://localhost:80
        启动服务的容器:docker-compose up ruoyi-mysql ruoyi-server
        停止服务的容器:docker-compose stop ruoyi-mysql ruoyi-server
    e.查看日志
        [root@bigdata03 docker]# docker logs 2f83c878adad
    f.navicat
        连接名:192.168.2.130
        主机:192.168.2.130
        端口:3306
        用户名:root
        密码:password

3.4 docker:ruoyi-vue

01.目录
    ...\集成DOCKER实现一键部署
    │  docker-compose.yml
    │  mysql-dockerfile
    │  nginx-dockerfile
    │  redis-dockerfile
    │  ruoyi-dockerfile
    │
    ├─conf
    │      nginx.conf
    │      redis.conf
    │
    ├─db
    ├─html
    │  └─dist
    └─jar

02.说明
    a.docker-compose.yml
        version : '3'
        services:
          ruoyi-mysql:
            container_name: ruoyi-mysql
            image: mysql:5.7
            build:
              context: .
              dockerfile: mysql-dockerfile
            ports:
              - "3306:3306"
            volumes:
              - ./mysql/conf:/etc/mysql/conf.d
              - ./mysql/logs:/logs
              - ./mysql/data:/var/lib/mysql
            command: [
                  'mysqld',
                  '--innodb-buffer-pool-size=80M',
                  '--character-set-server=utf8mb4',
                  '--collation-server=utf8mb4_unicode_ci',
                  '--default-time-zone=+8:00',
                  '--lower-case-table-names=1'
                ]
            environment:
              MYSQL_DATABASE: 'ry-vue'
              MYSQL_ROOT_PASSWORD: password
          ruoyi-redis:
            container_name: ruoyi-redis
            image: redis
            build:
              context: .
              dockerfile: redis-dockerfile
            ports:
              - "6379:6379"
            volumes:
              - ./conf/redis.conf:/home/ruoyi/redis/redis.conf
              - ./redis/data:/data
            command: redis-server /home/ruoyi/redis/redis.conf
          ruoyi-nginx:
            container_name: ruoyi-nginx
            image: nginx
            build:
              context: .
              dockerfile: nginx-dockerfile
            ports:
              - "80:80"
            volumes:
              - ./html/dist:/home/ruoyi/projects/ruoyi-ui
              - ./conf/nginx.conf:/etc/nginx/nginx.conf
              - ./nginx/logs:/var/log/nginx
              - ./nginx/conf.d:/etc/nginx/conf.d
            depends_on:
              - ruoyi-server
            links:
              - ruoyi-server
          ruoyi-server:
            container_name: ruoyi-server
            build:
              context: .
              dockerfile: ruoyi-dockerfile
            ports:
              - "8080:8080"
            volumes:
              - ./ruoyi/logs:/home/ruoyi/logs
              - ./ruoyi/uploadPath:/home/ruoyi/uploadPath
            depends_on:
              - ruoyi-mysql
              - ruoyi-redis
            links:
              - ruoyi-mysql
              - ruoyi-redis
    b.mysql-dockerfile
        # 基础镜像
        FROM mysql:5.7
        # author
        MAINTAINER ruoyi

        # 执行sql脚本
        ADD ./db/*.sql /docker-entrypoint-initdb.d/
    c.nginx-dockerfile
        # 基础镜像
        FROM nginx
        # author
        MAINTAINER ruoyi

        # 挂载目录
        VOLUME /home/ruoyi/projects/ruoyi-ui
        # 创建目录
        RUN mkdir -p /home/ruoyi/projects/ruoyi-ui
        # 指定路径
        WORKDIR /home/ruoyi/projects/ruoyi-ui
        # 复制conf文件到路径
        COPY ./conf/nginx.conf /etc/nginx/nginx.conf
        # 复制html文件到路径
        COPY ./html/dist /home/ruoyi/projects/ruoyi-ui
    d.redis-dockerfile
        # 基础镜像
        FROM redis
        # author
        MAINTAINER ruoyi

        # 挂载目录
        VOLUME /home/ruoyi/redis
        # 创建目录
        RUN mkdir -p /home/ruoyi/redis
        # 指定路径
        WORKDIR /home/ruoyi/redis
        # 复制conf文件到路径
        COPY ./conf/redis.conf /home/ruoyi/redis/redis.conf
    e.ruoyi-dockerfile
        # 基础镜像
        FROM java:8
        # author
        MAINTAINER ruoyi

        # 挂载目录
        VOLUME /home/ruoyi
        # 创建目录
        RUN mkdir -p /home/ruoyi
        # 指定路径
        WORKDIR /home/ruoyi
        # 复制jar文件到路径
        COPY ./jar/*.jar /home/ruoyi/ruoyi.jar
        # 启动应用
        ENTRYPOINT ["java","-jar","ruoyi.jar"]
    f.nginx.conf
        worker_processes  1;

        events {
            worker_connections  1024;
        }

        http {
            include       mime.types;
            default_type  application/octet-stream;
            sendfile        on;
            keepalive_timeout  65;

            server {
                listen       80;
                server_name  localhost;

                location / {
                    root   /home/ruoyi/projects/ruoyi-ui;
                    try_files $uri $uri/ /index.html;
                    index  index.html index.htm;
                }

                location /prod-api/{
                    proxy_set_header Host $http_host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header REMOTE-HOST $remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_pass http://ruoyi-server:8080/;
                }

                error_page   500 502 503 504  /50x.html;
                location = /50x.html {
                    root   html;
                }
            }
        }
    g.redis.conf
        # requirepass 123456

03.修改配置
    a.application.yml
        # 项目相关配置
        ruoyi:
          # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
          profile: /home/ruoyi/uploadPath

          redis:
            # 地址
            host: ruoyi-redis
            # 端口,默认为6379
            port: 6379
            # 数据库索引
            database: 0
            # 密码
            password:
            # 连接超时时间
            timeout: 10s
            lettuce:
              pool:
                # 连接池中的最小空闲连接
                min-idle: 0
                # 连接池中的最大空闲连接
                max-idle: 8
                # 连接池的最大数据库连接数
                max-active: 8
                # #连接池最大阻塞等待时间(使用负值表示没有限制)
                max-wait: -1ms
    b.application-druid.yml
        spring:
            datasource:
                type: com.alibaba.druid.pool.DruidDataSource
                driverClassName: com.mysql.cj.jdbc.Driver
                druid:
                    # 主库数据源
                    master:
                        url: jdbc:mysql://ruoyi-mysql:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                        username: root
                        password: password

04.docker操作
    a.启动docker
        [root@bigdata03 ~]# systemctl start docker
    b.构建docker服务
        [root@bigdata03 ~]# cd /home/docker/
        -----------------------------------------------------------------------------------------------------
        [root@bigdata03 docker]# ll
        total 20
        drwxr-xr-x 2 root root   42 Jul 28 17:57 conf
        drwxr-xr-x 2 root root   47 Jul 28 17:57 db
        -rw-r--r-- 1 root root 1784 Jul 28 17:57 docker-compose.yml
        drwxr-xr-x 3 root root   18 Jul 28 17:57 html
        drwxr-xr-x 2 root root   29 Jul 28 17:57 jar
        -rw-r--r-- 1 root root  126 Jul 28 17:57 mysql-dockerfile
        -rw-r--r-- 1 root root  379 Jul 28 17:57 nginx-dockerfile
        -rw-r--r-- 1 root root  273 Jul 28 17:57 redis-dockerfile
        -rw-r--r-- 1 root root  296 Jul 28 17:57 ruoyi-dockerfile
        -----------------------------------------------------------------------------------------------------
        [root@bigdata03 docker]# docker-compose build
        Building ruoyi-mysql
        Step 1/3 : FROM mysql:5.7
    c.启动docker容器
        [root@bigdata03 docker]# docker-compose up -d
        Creating network "docker_default" with the default driver
        Creating ruoyi-mysql ... done
        Creating ruoyi-redis ... done
        Creating ruoyi-server ... done
        Creating ruoyi-nginx  ... done
    d.访问
        http://localhost:80
        启动服务的容器:docker-compose up ruoyi-mysql ruoyi-server ruoyi-nginx ruoyi-redis
        停止服务的容器:docker-compose stop ruoyi-mysql ruoyi-server ruoyi-nginx ruoyi-redis
    e.查看日志
        [root@bigdata03 docker]# docker logs 2f83c878adad
    f.navicat
        连接名:192.168.2.130
        主机:192.168.2.130
        端口:3306
        用户名:root
        密码:password
    g.redis
        Host:192.168.2.130
        Host:6379

3.5 docker:eblog

01.配置
    a.Docker
        docker pull mysql:5.7.27
        docker pull redis:latest
        docker pull rabbitmq:management
        docker pull elasticsearch:6.4.3
    b.前端配置(eblog\src\main\resources\static\res\js\im.js)
        var url = "ws://192.168.2.128:9326?userId=" + self.userId;
    c.后端配置(application.yml)
        spring:
          datasource:
            driver-class-name: com.p6spy.engine.spy.P6SpyDriver
            url: jdbc:p6spy:mysql://emysql:3306/eblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
            username: root
            password: 4023615
          freemarker:
            settings:
              classic_compatible: true
              datetime_format: yyyy-MM-dd HH:mm
              number_format: 0.##
          redis:
            host: eredis
            port: 6379
            password: myslayers
          data:
            elasticsearch:
              cluster-name: kobe
              cluster-nodes: ees:9300
              repositories:
                enabled: true
          rabbitmq:
            username: guest
            password: guest
            host: erabbit
            port: 5672
        mybatis-plus:
          mapper-locations: classpath*:/mapper/**Mapper.xml
        file:
          upload:
            dir: ${user.dir}/upload
        im:
          server:
            port: 9326

02.部署
    a.Dockerfile
        FROM java:8
        EXPOSE 8080
        VOLUME /tmp
        ENV TZ=Asia/Shanghai
        RUN ln -sf /usr/share/zoneinfo/{TZ} /etc/localtime && echo "{TZ}" > /etc/timezone
        ADD eblog-0.0.1-SNAPSHOT.jar /app.jar
        RUN bash -c 'touch /app.jar'
        ENTRYPOINT ["java","-jar","/app.jar"]
    b.部署
        docker build -t eblog .
        -----------------------------------------------------------------------------------------------------
        docker run -p 8080:8080 -p 9326:9326 --name eblog \
        --link myslayers-elasticsearch:ees \
        --link myslayers-rabbit:erabbit \
        --link myslayers-mysql:emysql \
        --link myslayers-redis:eredis \
        -d eblog
        -----------------------------------------------------------------------------------------------------
        docker logs -f eblog
    c.测试
        http://192.168.2.128:8080/

4 Chapter04

4.1 购物车和订单功能

4.1.1 AuthAccess注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthAccess {
}

-------------------------------------------------------------------------------------------------------------

public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private IUserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("token");
        if (StrUtil.isBlank(token)) {
            token = request.getParameter("token");
        }
        // 如果不是映射到方法直接通过
        if(handler instanceof HandlerMethod) {
            AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class);
            if (annotation != null) {
                return true;
            }
        }
        // 执行认证
        if (StrUtil.isBlank(token)) {
            throw new ServiceException(Constants.CODE_401, "无token,请重新登录");
        }
        // 获取 token 中的 user id
        String userId;
        try {
            userId = JWT.decode(token).getAudience().get(0);
        } catch (JWTDecodeException j) {
            throw new ServiceException(Constants.CODE_401, "token验证失败,请重新登录");
        }
        // 根据token中的userid查询数据库
        User user = userService.getById(userId);
        if (user == null) {
            throw new ServiceException(Constants.CODE_401, "用户不存在,请重新登录");
        }
        // 用户密码加签验证 token
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
        try {
            jwtVerifier.verify(token); // 验证token
        } catch (JWTVerificationException e) {
            throw new ServiceException(Constants.CODE_401, "token验证失败,请重新登录");
        }
        return true;
    }
}

4.1.2 购物车

01.商品详情页
-> 加入购物车(按钮)
-> this.form(goods.id, num) 
-> save(不产生新记录,重复商品num+1,然后更新,而不是产生新纪录)
-> cart表
  id goods_id user_id num time
  10	1	1	3	2023-10-23 17:49:56

-------------------------------------------------------

<template>
  <div style="padding: 10px">
    <el-card>
      <div style="display: flex;">
        <div style="width: 300px">
          <el-image :src="goods.img" :preview-src-list="[goods.img]" style="width: 100%"></el-image>
        </div>
        <div style="flex: 1; padding-left: 50px">
          <div class="item1"><h3>{{ goods.name }}</h3></div>
          <div class="item1" style="font-size: 14px; ">{{ goods.description }}</div>
          <div class="item1" style="color: orangered">价格 ¥ {{ goods.price }}</div>
          <div class="item1" style="font-size: 14px; ">库存 {{ goods.store }} <span style="margin-left: 10px">{{ goods.unit }}</span></div>
          <div class="item1">
            <el-input-number v-model="form.num" :min="1" :max="100" label="数量"></el-input-number>
            <span style="margin-left: 10px">{{ goods.unit }}</span>
          </div>
          <div class="item1" style="margin-top: 40px">
            <el-button size="medium" style="background-color: red; color: white"><i class="el-icon-wallet"></i> 直接购买</el-button>
            <el-button size="medium" style="background-color: red; color: white" v-on:click="addCart"><i class="el-icon-shopping-cart-2"></i> 加入购物车</el-button>
          </div>
        </div>
      </div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: "Detail",
  data() {
    return {
      id: this.$route.query.id,
      goods: {},
      form: { num: 1 },
      user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {}
    }
  },
  created() {
    this.load()
  },
  methods: {
    load() {
      this.request.get("/goods/" + this.id).then(res => {
        this.goods = res.data
      })
    },
    addCart() {
      if(!this.user.username) {
        this.$message.warning("请登录后操作")
        return
      }
      this.form.goodsId = this.goods.id  // 商品id
      this.request.post("/cart", this.form).then(res => {
        if (res.code === "200") {
          this.$message.success("加入购物车成功")
        } else {
          this.$message.error(res.msg)
        }
      })
    }
  }
}
</script>

-------------------------------------------------------

@RestController
@RequestMapping("/cart")
public class CartController {

    // 新增或者更新
    @PostMapping
    public Result save(@RequestBody Cart cart) {
        Integer userId = TokenUtils.getCurrentUser().getId();
        // 更新购物车里相同的商品的数量
        Integer goodsId = cart.getGoodsId();

        QueryWrapper<Cart> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", userId);
        queryWrapper.eq("goods_id", goodsId);
        Cart db = cartService.getOne(queryWrapper);
        if (db != null) {
            db.setNum(db.getNum() + cart.getNum());
            cartService.updateById(db);
            return Result.success();
        }

        // 新增或更新
        if (cart.getId() == null) {
            cart.setTime(DateUtil.now());
            cart.setUserId(userId);
        }
        cartService.saveOrUpdate(cart);
        return Result.success();
    }
}

-------------------------------------------------------------------------------------------------------------

购物车:列表

load() {
  this.request.get("/cart/page", {
    params: {
      pageNum: this.pageNum,
      pageSize: this.pageSize,
      name: this.name,
    }
  }).then(res => {
    this.tableData = res.data.records
    this.total = res.data.total
  })
},

@RestController
@RequestMapping("/cart")
public class CartController {

    @GetMapping("/page")
    public Result findPage(@RequestParam(defaultValue = "") String name,
                           @RequestParam Integer pageNum,
                           @RequestParam Integer pageSize) {
        User currentUser = TokenUtils.getCurrentUser();
        Integer userId = currentUser.getId();
        String role = currentUser.getRole();

        // name是商品的名称
        return Result.success(cartMapper.page(new Page<>(pageNum, pageSize), userId, role, name));
    }
}

<select id="page" resultType="com.example.springboot.entity.Cart">
    select c.*, g.name as goodsName, g.img as goodsImg, g.price, u.username, u.nickname from cart c
    left join goods g on c.goods_id = g.id
    left join sys_user u on c.user_id = u.id
    <where>
        <if test="name != null and name != ''">
            and g.name = like concat('%', #{name}, '%')
        </if>
        <if test="role == 'ROLE_USER'">
            and c.user_id = #{userId}
        </if>
    </where>
</select>

-------------------------------------------------------

购物车:删除商品

del(id) {
  this.request.delete("/cart/" + id).then(res => {
    if (res.code === '200') {
      this.$message.success("删除成功")
      this.load()
    } else {
      this.$message.error("删除失败")
    }
  })
},

@RestController
@RequestMapping("/cart")
public class CartController {

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        cartService.removeById(id);
        return Result.success();
    }
        
    @PostMapping("/del/batch")
    public Result deleteBatch(@RequestBody List<Integer> ids) {
        cartService.removeByIds(ids);
        return Result.success();
    }
}

-------------------------------------------------------

购物车:商品数量增加/减少

changeNum(row) {
  console.log(row)
  this.request.post("/cart/num/" + row.id + "/" + row.num).then(res => {
    this.totalPrice = 0
    // 计算总价格
    this.multipleSelection.forEach(item => {
      item.num = row.num  // 更新选中的数组的数量
      this.totalPrice += item.price * row.num  // 这个地方小坑,因为我们的数字发生变化了,需要取到最新的值,进行计算
    })
    this.totalPrice = this.totalPrice.toFixed(2)
  })
},

@RestController
@RequestMapping("/cart")
public class CartController {

    @PostMapping("/num/{id}/{num}")
    public Result updateNum(@PathVariable Integer id, @PathVariable Integer num) {
        cartMapper.updateNum(num, id);
        return Result.success();
    }
}

public interface CartMapper extends BaseMapper<Cart> {

    @Update("update cart set num = #{num} where id = #{id}")
    void updateNum(@Param("num") Integer num, @Param("id") Integer id);
}

-------------------------------------------------------

购物车:勾选全部,显示总价

<div style="padding: 10px 0">当前已选商品总价:<span style="color: orangered">¥ {{ totalPrice }}</span></div>

<el-table :data="tableData" border stripe size="medium"  @selection-change="handleSelectionChange">

handleSelectionChange(val) {
  this.totalPrice = 0
  this.multipleSelection = val
  // 进行计算操作
  if (val && val.length) {
    val.forEach(item => {
      this.totalPrice += item.num * item.price
    })
    this.totalPrice = this.totalPrice.toFixed(2)
  }
},

-------------------------------------------------------

购物车:结算功能

<el-button style="color: white; background-color: orangered" size="medium"  @click="settleAccount"><i class="el-icon-coin"></i> 结 算</el-button>

settleAccount() {
  if (!this.multipleSelection || !this.multipleSelection.length) {
    this.$message.error("请选择需要结算的商品")
    return
  }
  let data = {name: this.multipleSelection[0].goodsName, totalPrice: this.totalPrice, carts: this.multipleSelection}
  this.request.post('/orders', data).then(res => {
    if (res.code === '200') {
      this.$message.success("结算成功")
      this.load()
    } else {
      this.$message.error(res.msg)
    }
  })
},

@RestController
@RequestMapping("/orders")
public class OrdersController {
	
    // 新增或者更新
    @PostMapping
    public Result save(@RequestBody Orders orders) {
        if (orders.getId() == null) {
            Date date = new Date();
            orders.setTime(DateUtil.formatDateTime(date));
            orders.setNo(DateUtil.format(date, "yyyyMMdd") + System.currentTimeMillis());
            orders.setUserId(TokenUtils.getCurrentUser().getId());
            // 新增订单:orders
            ordersService.save(orders);
            List<Cart> carts = orders.getCarts();
            for (Cart cart : carts) {
                OrdersGoods ordersGoods = new OrdersGoods();
                ordersGoods.setGoodsId(cart.getGoodsId());
                ordersGoods.setNum(cart.getNum());
                ordersGoods.setOrderId(orders.getId());
                // 添加数据:orders_goods中间表
                ordersGoodsService.save(ordersGoods);

                // 删除数据:card购物车
                cartService.removeById(cart.getId());
            }
        } else {
            ordersService.updateById(orders);
        }
        return Result.success();
    }
}

4.1.3 订单功能

订单:列表

load() {
  this.request.get("/orders/page", {
    params: {
      pageNum: this.pageNum,
      pageSize: this.pageSize,
      name: this.name,
    }
  }).then(res => {
    this.tableData = res.data.records
    this.total = res.data.total
  })
},

@RestController
@RequestMapping("/orders")
public class OrdersController {
    @GetMapping("/page")
    public Result findPage(@RequestParam(defaultValue = "") String name,
                           @RequestParam Integer pageNum,
                           @RequestParam Integer pageSize) {
        QueryWrapper<Orders> queryWrapper = new QueryWrapper<>();
        queryWrapper.orderByDesc("id");
        if (!"".equals(name)) {
            queryWrapper.like("name", name);
        }
        User currentUser = TokenUtils.getCurrentUser();
        return Result.success(ordersMapper.page(new Page<>(pageNum, pageSize), name, currentUser.getRole(), currentUser.getId()));
    }
}

<select id="page" resultType="com.example.springboot.entity.Orders">
    select o.*, u.username, u.nickname from orders o left join sys_user u on o.user_id = u.id
    <where>
        <if test="name != ''">
            and o.name like concat('%', #{name}, '%')
        </if>
        <if test="role == 'ROLE_USER'">
             and o.user_id = #{userId}
        </if>
    </where>
</select>

-------------------------------------------------------------------------------------------------------------

订单:删除订单(删除orders、orders_goods)

del(id) {
  this.request.delete("/orders/" + id).then(res => {
    if (res.code === '200') {
      this.$message.success("删除成功")
      this.load()
    } else {
      this.$message.error("删除失败")
    }
  })
},

delBatch() {
  if (!this.multipleSelection.length) {
    this.$message.error("请选择需要删除的数据")
    return
  }
  let ids = this.multipleSelection.map(v => v.id)  // [{}, {}, {}] => [1,2,3]
  this.request.post("/orders/del/batch", ids).then(res => {
    if (res.code === '200') {
      this.$message.success("批量删除成功")
      this.load()
    } else {
      this.$message.error("批量删除失败")
    }
  })
},

@RestController
@RequestMapping("/orders")
public class OrdersController {
    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id) {
        ordersService.removeById(id);
        return Result.success();
    }

    @PostMapping("/del/batch")
    public Result deleteBatch(@RequestBody List<Integer> ids) {
        ordersService.removeByIds(ids);
        return Result.success();
    }
}

-------------------------------------------------------------------------------------------------------------

订单:查看订单下的全部商品

viewGoods(orderId) {
  this.request.get("/orders/getGoodsById/" + orderId).then(res => {
    this.goodsList = res.data
    this.dialogFormVisible1 = true
    // setTimeout(() => {
    //   this.dialogFormVisible1 = false
    // }, 3000)
  })
},

@RestController
@RequestMapping("/orders")
public class OrdersController {
    @GetMapping("/getGoodsById/{id}")
    public Result getGoodsById(@PathVariable Integer id) {
        QueryWrapper<OrdersGoods> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_id", id);
        ArrayList<Goods> goodsList = new ArrayList<>();
        List<OrdersGoods> ordersGoodsList = ordersGoodsService.list(queryWrapper);
        for (OrdersGoods ordersGoods : ordersGoodsList) {
            Integer goodsId = ordersGoods.getGoodsId();
            Goods goods = goodsService.getById(goodsId);
            goods.setNum(ordersGoods.getNum());
            goodsList.add(goods);
        }
        return Result.success(goodsList);
    }
}

4.2 审核功能

CREATE TABLE `goods` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '物品名称',
  `user` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '所属人',
  `img` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '物品图片',
  `time` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '报修时间',
  `state` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '待审核' COMMENT '报修状态',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-------------------------------------------------------------------------------------------------------------

// 新增或者更新
@PostMapping
public Result save(@RequestBody Goods goods) {
    if (goods.getId() == null) {
        // 新增
        goods.setTime(DateUtil.today());
        goods.setUser(TokenUtils.getCurrentUser().getUsername());
    }
    goodsService.saveOrUpdate(goods);
    return Result.success();
}

// 分页查询做一下筛选
@GetMapping("/page")
public Result findPage(@RequestParam(defaultValue = "") String name,
                       @RequestParam Integer pageNum,
                       @RequestParam Integer pageSize) {
    QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
    queryWrapper.orderByDesc("id");
    if (!"".equals(name)) {
        queryWrapper.like("name", name);
    }
    User currentUser = TokenUtils.getCurrentUser();
    if (RoleEnum.ROLE_USER.toString().equals(currentUser.getRole())) {  // 角色是普通用户
        queryWrapper.eq("user", currentUser.getUsername());
    }
    return Result.success(goodsService.page(new Page<>(pageNum, pageSize), queryWrapper));
}

-------------------------------------------------------------------------------------------------------------

<el-table-column label="审核" v-if="user.role === 'ROLE_ADMIN'" width="240">
  <template v-slot="scope">
    <el-button type="success" @click="changeState(scope.row, '审核通过')" :disabled="scope.row.state !== '待审核'">审核通过</el-button>
    <el-button type="danger" @click="changeState(scope.row, '审核不通过')" :disabled="scope.row.state !== '待审核'">审核不通过</el-button>
  </template>
</el-table-column>

<!-- methods里加入这个方法就行了  -->
changeState(row, state) {
  this.form = JSON.parse(JSON.stringify(row))
  this.form.state = state;
  this.save();
},

4.3 Echarts图表

<template>
  <div>
    <el-row :gutter="10">
      <el-col :span="12">
        <el-card>
          <div style="width: 100%; height: 400px" id="line"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div style="width: 100%; height: 400px" id="bar"></div>
        </el-card>
      </el-col>
    </el-row>

    <el-row :gutter="10" style="margin: 10px 0">
      <el-col :span="12">
        <el-card>
          <div style="width: 100%; height: 400px" id="pie"></div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import * as echarts from 'echarts'

const option = {
  title: {
    text: '订单销售的趋势图',
    left: 'center'
  },
  tooltip: {
    trigger: 'axis'
  },
  legend: {
    left: 'left'
  },
  xAxis: {
    type: 'category',
    data: []
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: '金额',
      data: [],
      type: 'line',
      smooth: true
    },
  ]
}

const option1 = {
  title: {
    text: '订单销售的柱状图',
    left: 'center'
  },
  tooltip: {
    trigger: 'axis'
  },
  legend: {
    left: 'left'
  },
  xAxis: {
    type: 'category',
    data: []
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: '金额',
      data: [],
      type: 'bar',
      smooth: true
    }
  ]
}

const option2 = {
  title: {
    text: '订单销售统计',
    subtext: '比例图',
    left: 'center'
  },
  tooltip: {
    trigger: 'item'
  },
  legend: {
    orient: 'vertical',
    left: 'left'
  },
  series: [
    {
      name: '金额',
      type: 'pie',
      center: ['50%', '60%'],
      radius: '50%',
      data: [],
      label: {
        show: true,
        formatter(param) {
          return param.name + ' (' + param.percent + '%)';
        }
      },
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }
  ]
}

export default {
  name: "Charts",
  data() {
    return {}
  },
  mounted() {
    this.$request.get('/charts').then(res => {
      // 折线
      let lineChart = echarts.init(document.getElementById('line'));
      option.xAxis.data = res.data?.line?.map(v => v.date) || []
      option.series[0].data = res.data?.line?.map(v => v.value) || []
      lineChart.setOption(option)

      // 柱状图
      let barChart = echarts.init(document.getElementById('bar'));
      option1.xAxis.data = res.data?.bar?.map(v => v.name) || []
      option1.series[0].data = res.data?.bar?.map(v => v.value) || []
      barChart.setOption(option1)

      // 饼图
      let pieChart = echarts.init(document.getElementById('pie'));
      option2.series[0].data = res.data?.bar || []
      pieChart.setOption(option2)
    })
  },
  methods: {}
}
</script>

5 Chapter05

5.1 定时任务

@Async异步方法默认使用Spring创建ThreadPoolTaskExecutor(参考TaskExecutionAutoConfiguration),
其中默认核心线程数为8, 默认最大队列和默认最大线程数都是Integer.MAX_VALUE。
创建新线程的条件是队列填满时, 而这样的配置队列永远不会填满,如果有@Async注解标注的方法长期占用线程(比如HTTP长连接等待获取结果),
在核心8个线程数占用满了之后, 新的调用就会进入队列, 外部表现为没有执行.

-------------------------------------------------------------------------------------------------------------

@EnableScheduling  //开启任务调度
@SpringBootApplication
@ComponentScan("com.sxoms.ws.task")
@Slf4j
public class SkyApplication {

    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

-------------------------------------------------------------------------------------------------------------

@Slf4j
@Component
public class MyTask {

    @Scheduled(cron = "0/5 * * * * ?")
    public void executeTask(){
        log.info("定时任务开始执行:{}", new Date());
    }

}

-------------------------------------------------------------------------------------------------------------

@Slf4j
@Component
@EnableAsync
@Configuration
@EnableScheduling
public class MyTask {

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(14);
        executor.setMaxPoolSize(50);
        executor.setThreadNamePrefix("tmxtxjx-task-18080-exec-");
        return executor;
    }

    @Async("taskExecutor")
    @Scheduled(cron = "0/5 * * * * ?")
    public void executeTask(){
        log.info("定时任务开始执行:{}", new Date());
    }
}

-------------------------------------------------------------------------------------------------------------

public static void main(String[] args) {
    // 创建一个固定大小的线程池,这里设置为10个线程
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    // 提交一系列任务给线程池
    for (int i = 0; i < 20; i++) {
        // 这里使用Lambda表达式创建一个简单的任务
        Runnable task = () -> {
            // 执行任务的逻辑,这里简单地打印当前线程的名称
            System.out.println("Task executed on thread: " + Thread.currentThread().getName());
        };
        // 提交任务给线程池
        executorService.submit(task);
    }
    // 关闭线程池
    executorService.shutdown();
}

5.2 处理全局异常

/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler(BaseException.class)
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    /**
     * 处理SQL异常
     * @param ex
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        //Duplicate entry 'zhangsan' for key 'employee.idx_username'
        String message = ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
}
01.使用@ExceptionHandler注解
    @Controller      
    public class GlobalController {               

       /**    
         * 用于处理异常的
         * @return    
         */      
        @ExceptionHandler({MyException.class})       
        public String exception(MyException e) {       
            System.out.println(e.getMessage());       
            e.printStackTrace();       
            return "exception";       
        }       

        @RequestMapping("test")       
        public void test() {       
            throw new MyException("出错了!");       
        }                    
    }
    -----------------------------------------------------------------------------------------------------
    使用该注解有一个不好的地方就是:进行异常处理的方法必须与出错的方法在同一个Controller里面
    可以看到,这种方式最大的缺陷就是不能全局控制异常。每个类都要写一遍。

02.实现HandlerExceptionResolver接口
    @Component  
    public class ExceptionTest implements HandlerExceptionResolver{  

        /**  
         * TODO 简单描述该方法的实现功能(可选).  
         * @see org.springframework.web.servlet.HandlerExceptionResolver#resolveException(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object, java.lang.Exception)  
         */   
        public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,  
                Exception ex) {  
            System.out.println("This is exception handler method!");  
            return null;  
        }  
    }
    -----------------------------------------------------------------------------------------------------
    这种方式可以进行全局的异常控制

03.使用@controlleradvice注解
    @ControllerAdvice
    @ResponseBody
    public class WebExceptionHandle {
        private static Logger logger = LoggerFactory.getLogger(WebExceptionHandle.class);
        /**
         * 400 - Bad Request
         */
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ExceptionHandler(HttpMessageNotReadableException.class)
        public ServiceResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
            logger.error("参数解析失败", e);
            return ServiceResponseHandle.failed("could_not_read_json");
        }

        /**
         * 405 - Method Not Allowed
         */
        @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
        @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
        public ServiceResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
            logger.error("不支持当前请求方法", e);
            return ServiceResponseHandle.failed("request_method_not_supported");
        }

        /**
         * 415 - Unsupported Media Type
         */
        @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
        @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
        public ServiceResponse handleHttpMediaTypeNotSupportedException(Exception e) {
            logger.error("不支持当前媒体类型", e);
            return ServiceResponseHandle.failed("content_type_not_supported");
        }

        /**
         * 500 - Internal Server Error
         */
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        @ExceptionHandler(Exception.class)
        public ServiceResponse handleException(Exception e) {
            if (e instanceof BusinessException){
                return ServiceResponseHandle.failed("BUSINESS_ERROR", e.getMessage());
            }

            logger.error("服务运行异常", e);
            e.printStackTrace();
            return ServiceResponseHandle.failed("server_error");
        }
    }

5.3 跨域的3种方案

01.第1种
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("*")
                    .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                    .allowCredentials(true)
                    .maxAge(3600)
                    .allowedHeaders("*");
        }
    }

02.第2种
    @WebFilter(filterName = "CorsFilter ")
    @Configuration
    public class CorsFilter implements Filter {
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
            HttpServletResponse response = (HttpServletResponse) res;
            response.setHeader("Access-Control-Allow-Origin","*");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
            chain.doFilter(req, res);
        }
    }

03.第3种
    public class GoodsController {
    @CrossOrigin(origins = "http://localhost:4000")
    @GetMapping("goods-url")
    public Response queryGoodsWithGoodsUrl(@RequestParam String goodsUrl) throws Exception {}

    }

5.4 MyBatisPlus分页

@Configuration
@MapperScan("scan.your.mapper.package")
public class MybatisPlusConfig {

    /**
     * 添加分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));  //如果配置多个插件,切记分页最后添加
        return interceptor;
    }
}

5.5 RdisTemplate

5.5.1 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

5.5.2 配置文件

spring:
  redis:
      host: localhost
      port: 6379
      password: myslayers
      database: 0

5.5.3 注入IOC容器

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化器
        template.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

5.6 PageHelper

5.6.1 依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

5.6.2 实体类

@Data
public class DishPageQueryDTO implements Serializable {

    private int page;

    private int pageSize;

    private String name;

    //分类id
    private Integer categoryId;

    //状态 0表示禁用 1表示启用
    private Integer status;

}

-------------------------------------------------------------------------------------------------------------

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {

    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //更新时间
    private LocalDateTime updateTime;
    //分类名称
    private String categoryName;
    //菜品关联的口味
    private List<DishFlavor> flavors = new ArrayList<>();

    //private Integer copies;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

    private List records; //当前页数据集合

}
@Data
public class Result<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据

    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }

    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }

    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}

5.6.3 使用

@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
    log.info("菜品分页查询:{}", dishPageQueryDTO);
    PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
    return Result.success(pageResult);
}

-------------------------------------------------------------------------------------------------------------

public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
    PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
    Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
    return new PageResult(page.getTotal(), page.getResult());
}

-------------------------------------------------------------------------------------------------------------

Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

-------------------------------------------------------------------------------------------------------------

<select id="pageQuery" resultType="com.sky.vo.DishVO">
  select d.*, c.name as categoryName
  from dish d left outer join category c on d.category_id = c.id
  <where>
    <if test="name != null">
      and d.name like concat('%', #{name}, '%')
    </if>
    <if test="categoryId != null">
      and d.category_id = #{categoryId}
    </if>
    <if test="status != null">
      and d.status = #{status}
    </if>
  </where>
  order by d.create_time desc
</select>

5.6.4 后端分页

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;

import java.util.List;

public class YourService {
    
    // 例如,这是一个查询所有数据并分页的方法
    public List<YourEntity> getAllDataWithPage(int pageNum, int pageSize) {
        // 在查询之前调用 PageHelper.startPage 方法,传入页码和每页大小
        PageHelper.startPage(pageNum, pageSize);

        // 执行查询语句,获取数据列表
        List<YourEntity> dataList = yourMapper.selectAllData();

        // 使用 PageInfo 对象封装查询结果,其中包含分页信息
        PageInfo<YourEntity> pageInfo = new PageInfo<>(dataList);

        // 返回分页后的数据列表
        return pageInfo.getList();
    }
}

-------------------------------------------------------------------------------------------------------------

@Api("订单接口")
@RestController
@RequestMapping("/microsoft/order")
public class PurchaseOrderController extends BaseController {
    @PreAuthorize("@ss.hasPermi('microsoft:order:list')")
    @GetMapping("/list")
    public TableDataInfo orderList(PurchaseOrderRequest purchaseOrder) {
        PageHelper.startPage(purchaseOrder.pageNum, purchaseOrder.pageSize);
        List<PurchaseOrder> orderForTokenList = purchaseOrderService.orderList(purchaseOrder, purchaseOrder.label);
        return getDataTable(orderForTokenList);
    }
}

protected TableDataInfo getDataTable(List<?> list) {
    TableDataInfo rspData = new TableDataInfo();
    rspData.setCode(HttpStatus.SUCCESS);
    rspData.setMsg("查询成功");
    rspData.setRows(list);
    rspData.setTotal(new PageInfo(list).getTotal());
    return rspData;
}

-------------------------------------------------------------------------------------------------------------

<template>
  <div class="app-container">
    <!--  搜索栏  -->
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="PO单号" prop="poNumber">
        <el-input
          v-model.trim="queryParams.poNumber"
          placeholder="请输入PO单号"
          clearable
          @input="$forceUpdate"
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="客户名称" prop="cusName">
        <el-input
          v-model.trim="queryParams.cusName"
          placeholder="请输入客户名称"
          clearable
          @input="$forceUpdate"
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <!--  表格列表  -->
    <el-table v-loading="loading" :data="orderList" stripe border>
      <el-table-column label="PO单号" align="center" prop="poNumber" width="150px"/>
      <el-table-column label="客户名称" align="center" prop="cusName" width="150px"/>
      <el-table-column label="产品型号" align="center" prop="sku" width="100px"/>
    </el-table>

    <!--  分页  -->
    <pagination
      v-show="total>0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />
  </div>
</template>

export default {
  data() {
    return {
      queryParams: {
        label: '',
        pageNum: 1,
        pageSize: 10,
        poNumber: null,
        cusName: null,
      }
      // 总条数
      total: 0,
    }
  },
  
  created() {
    this.getList()
  },
  
  this.queryParams.label = 1
  listOrder(this.queryParams).then(response => {
    if (response.rows !== null && response.rows !== undefined && response.rows.length > 0) {
      response.rows.forEach(function(item) {
        // 1,等待采购
        // 2,采购完成
        // 3,部分交付
        // 4,全部交付
        // 5,部分退货
        // 6,全部退货
        // 7,采购失败
        // 8,退货失败
        // false,启用
        // true,禁用
        if (item.orderStatus == 1) {
          item.purchaseForbid = false
          item.tokenForbid = true
          item.editForbid = false
          item.delForbid = false
        } else if (item.orderStatus == 7) {
          item.purchaseForbid = false
          item.tokenForbid = false
          item.editForbid = true
          item.delForbid = true
        } else {
          item.purchaseForbid = true
          item.tokenForbid = false
          item.editForbid = true
          item.delForbid = true
        }
      })
      this.loading = false
      this.orderList = response.rows
      this.total = response.total
    } else {
      this.loading = false
      this.orderList = []
    }
  })
    	
export function listOrder(query) {
  return request({
    url: '/microsoft/order/list',
    method: 'get',
    params: query
  })
}

5.6.5 前端分页

前端分页:
如果前端已经获取了所有的数据,那么切换页面只需要在前端进行,不需要再次调用后端。
这种情况下,前端通过 JavaScript 等技术对数据进行分页操作,不再向后端发起请求。

后端分页:
如果数据量较大,而且前端不想一次性获取所有数据,那么可能会选择后端进行分页,
前端每次切换页码时都需要向后端发起请求获取相应页的数据。

-------------------------------------------------------------------------------------------------------------

<template>
  <div>
    <el-table :data="pagedData" border>
      <el-table-column prop="id" label="ID"></el-table-column>
      <el-table-column prop="name" label="Name"></el-table-column>
    </el-table>
    
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="currentPage"
      :page-sizes="[10, 20, 30, 40]"
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="totalItems"
    ></el-pagination>
  </div>
</template>

<script>
export default {
  data() {
    return {
      allData: [], // 从后端获取的所有数据
      currentPage: 1,
      pageSize: 10,
    };
  },
  computed: {
    // 计算属性,根据当前页和每页数量过滤数据
    pagedData() {
      const startIndex = (this.currentPage - 1) * this.pageSize;
      const endIndex = startIndex + this.pageSize;
      return this.allData.slice(startIndex, endIndex);
    },
    totalItems() {
      return this.allData.length;
    },
  },
  methods: {
    // 处理每页显示数量变化
    handleSizeChange(val) {
      this.pageSize = val;
    },
    // 处理当前页变化
    handleCurrentChange(val) {
      this.currentPage = val;
    },
    // 从后端获取数据的方法,可以在created钩子中调用
    fetchDataFromBackend() {
      // 使用axios或其他HTTP库从后端获取数据
      // 将数据赋值给this.allData
    },
  },
  created() {
    this.fetchDataFromBackend();
  },
};
</script>

-------------------------------------------------------------------------------------------------------------

<template>
  <div>
    <el-table :data="pagedData.slice((currentPage-1)*pageSize, currentPage*pageSize)" border>
      <el-table-column prop="id" label="ID"></el-table-column>
      <el-table-column prop="name" label="Name"></el-table-column>
    </el-table>
    
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="currentPage"
      :page-sizes="[10, 20, 30, 40]"
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="totalItems"
    ></el-pagination>
  </div>
</template>

<script>
export default {
  data() {
    return {
      allData: [], // 从后端获取的所有数据
      currentPage: 1,
      pageSize: 10,
      totalItems: 0,
    };
  },
  methods: {
    // 处理每页显示数量变化
    handleSizeChange(val) {
      this.pageSize = val;
    },
    // 处理当前页变化
    handleCurrentChange(val) {
      this.currentPage = val;
    },
    // 从后端获取数据的方法,可以在created钩子中调用
    fetchDataFromBackend() {
      const response = await axios.get('your_api_endpoint');
      // 将数据赋值给this.allData
      this.allData = response.data;
      // 将数据长度赋值给this.totalItems
      this.totalItems = response.data.length;
    },
  },
  created() {
    this.fetchDataFromBackend();
  },
};
</script>

5.7 SnowFlake

5.7.1 工具类

public class IdWorker {
    // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
    private final static long twepoch = 1288834974657L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
    // 机器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒内自增位
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生产id时间戳 */
    private static long lastTimestamp = -1L;
    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    // 数据标识id部分
    private final long datacenterId;

    public IdWorker(){
        this.datacenterId = getDatacenterId(maxDatacenterId);
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }
    /**
     * @param workerId
     *            工作机器ID
     * @param datacenterId
     *            序列号
     */
    public IdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    /**
     * 获取下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;

        return nextId;
    }

    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * <p>
     * 获取 maxWorkerId
     * </p>
     */
    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(datacenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
            /*
             * GET jvmPid
             */
            mpid.append(name.split("@")[0]);
        }
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    /**
     * <p>
     * 数据标识id部分
     * </p>
     */
    protected static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        } catch (Exception e) {
            System.out.println(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }
}

5.7.2 注入IOC容器

@Configuration
public class EmployeeApplication {
    @Bean
    public IdWorker idWorkker() {
        return new IdWorker(1, 1);
    }
}

5.7.3 测试

@Service
public class CompanyService {
    @Autowired
    private IdWorker idWorker;

    public void add(Company company) {
        //基本属性的设置
        String id = idWorker.nextId()+"";
        company.setId(id);
        //默认的状态
        company.setAuditState("0");//0:未审核,1:已审核
        company.setState(1); //0.未激活,1:已激活
        companyDao.save(company);
    }
}