• 【Java从零到架构师第③季】【项目实战】驾考管理系统



    持续学习&持续更新中…

    守破离


    【Github】项目源码地址:https://github.com/lpruoyu/JAVAEE_PROJECT_jiakao

    企业开发中常见的后台管理系统

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    技术栈及第三方库

    技术栈

    后台:

    • SpringBoot
    • MyBatisPlus、Druid
    • 权限控制:Shiro
    • 后端校验:HibernateValidator
    • 验证码:easy-captcha
    • 缓存:ehcache
    • 对象转换:MapStruct
    • 拼音相关:tinypinyin
    • 接口文档:Swagger
    • 代码生成:EasyCode

    前端:

    • layui(LayuiMini)
    • md5加密
    • Freemarker(项目初期使用,前后端分离后就不用了)
    • font-awesome

    pom.xml

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
        <groupId>programmer.lp.jkbe_v3groupId>
        <artifactId>JiaKaoBE_V3artifactId>
    
        <packaging>jarpackaging>
        <version>1.0.0version>
    
        <parent>
            <artifactId>spring-boot-starter-parentartifactId>
            <groupId>org.springframework.bootgroupId>
            <version>2.3.4.RELEASEversion>
        parent>
    
        <properties>
            <druid.version>1.2.1druid.version>
            <mybatis.plus.version>3.4.1mybatis.plus.version>
            <tinypinyin.version>2.0.3tinypinyin.version>
            <mapStruct.version>1.4.1.FinalmapStruct.version>
            <captcha.version>1.6.2captcha.version>
            <commons.io.version>2.11.0commons.io.version>
            <swagger.models.version>1.6.2swagger.models.version>
            <swagger.triui.version>1.9.6swagger.triui.version>
            <swagger.version>2.9.2swagger.version>
            <shiro.version>1.7.0shiro.version>
            <springfox.version>3.0.0springfox.version>
        properties>
    
        <dependencies>
    
    
    
    
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
    
    
    
    
    
    
            dependency>
    
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
            dependency>
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>druid-spring-boot-starterartifactId>
                <version>${druid.version}version>
            dependency>
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-boot-starterartifactId>
                <version>${mybatis.plus.version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-aopartifactId>
            dependency>
    
            
            <dependency>
                <groupId>com.github.promeggroupId>
                <artifactId>tinypinyinartifactId>
                <version>${tinypinyin.version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-validationartifactId>
            dependency>
    
            
            <dependency>
                <groupId>io.springfoxgroupId>
                <artifactId>springfox-swagger2artifactId>
                <version>${swagger.version}version>
            dependency>
            <dependency>
                <groupId>io.springfoxgroupId>
                <artifactId>springfox-swagger-uiartifactId>
                <version>${swagger.version}version>
            dependency>
            <dependency>
                <groupId>io.swaggergroupId>
                <artifactId>swagger-modelsartifactId>
                <version>${swagger.models.version}version>
            dependency>
            <dependency>
                <groupId>com.github.xiaoymingroupId>
                <artifactId>swagger-bootstrap-uiartifactId>
                <version>${swagger.triui.version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.mapstructgroupId>
                <artifactId>mapstructartifactId>
                <version>${mapStruct.version}version>
                <scope>providedscope>
            dependency>
            <dependency>
                <groupId>org.mapstructgroupId>
                <artifactId>mapstruct-processorartifactId>
                <version>${mapStruct.version}version>
                <scope>providedscope>
            dependency>
    
            
            <dependency>
                <groupId>com.github.whvcsegroupId>
                <artifactId>easy-captchaartifactId>
                <version>${captcha.version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.ehcachegroupId>
                <artifactId>ehcacheartifactId>
            dependency>
    
            
            <dependency>
                <groupId>org.apache.shirogroupId>
                <artifactId>shiro-spring-boot-web-starterartifactId>
                <version>${shiro.version}version>
            dependency>
    
            
            <dependency>
                <groupId>commons-iogroupId>
                <artifactId>commons-ioartifactId>
                <version>${commons.io.version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-configuration-processorartifactId>
                <scope>providedscope>
            dependency>
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <scope>providedscope>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-devtoolsartifactId>
                <scope>providedscope>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
    
    
    
    
    
    
    
    
    
    
        dependencies>
        <build>
            <finalName>jkfinalName>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                plugin>
            plugins>
        build>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190

    各种Object

    • 数据库中对于一张表的数据,由于拥有隐私字段、多余字段、字段过少等原因,不应该直接传递给客户端让客户端直接使用。
    • 并且要知道数据的传输是要经过网络通信的,考虑到数据大小对于用户流量、系统并发量/吞吐量等的影响,我们也不应该给客户传递冗余或者缺失的JSON数据。
    • 还有很多原因,比如我们的一个业务所需要的信息有时候其实并不是仅由一张表就能覆盖的、比如数据库中的字段信息其实并不适合展示给用户看,需要做处理、…
    • 综上,我们肯定要对从数据库中查询出来的表数据进行一些加工处理、业务逻辑处理之后再传递给上一层,直到客户端,而不是简单的一张表对应一个Model对象。
      在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    前后端分离

    在这里插入图片描述

    以前这种协作模式的问题:

    • 前端地位比较低,大部分工作都在后台
    • 调试、修改页面比较麻烦,需要前端和后台相互充分配合修改动态模板
    • 浪费用户流量以及增加服务器负担:每次请求服务器都会返回整个HTML页面

    前后端分离—实现

    1. 前端页面保存使用静态页面HTML
    2. 静态页面使用JS发送异步请求(AJAX)给后台服务器,后台服务器返回JSON数据
    3. 前端使用JS解析JSON数据,动态生成HTML标签,显示在浏览器给用户展示
    4. 前端一个项目、后端一个项目,各自独立开发
    5. 前端一个服务器、后端一个服务器,分开部署
      在这里插入图片描述
      在这里插入图片描述

    前后端分离—约定数据格式

    • 项目开发应该撰写开发文档,前端照着开发文档编写页面,后端照着开发文档编写API接口
    • 后端和前端之间应该有一种数据格式、有一种协议,双方事先约定好一种数据格式,来规范、辅助开发,比如让后端返回JSON数据
    • 比如后端约定好给前端返回:
      # 错误的返回结果,并且设置HTTP状态码为400/500...
      {
      	"code": 8001,
      	"msg": "密码错误"
      }
      
      # 正确的返回结果
      {
      	"code": 8000,
      	"msg": "添加成功",
      	"data": [ ... ]
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

    跨域问题

    • https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

    • https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS/Errors/CORSMissingAllowOrigin

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    @RestController
    //@CrossOrigin("*") // 所有的源都可以跨域访问该Controller下的请求
    //@CrossOrigin({"http://localhost:63343","http://192.168.152.130:8888"})
    //@CrossOrigin("http://localhost:63343")
    public class UserController {
    
        @GetMapping("/users")
        @CrossOrigin("http://localhost:63343")
        public List<User> user() {
            List<User> list = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                list.add(new User("lp" + i, i));
            }
            return list;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
    //        registry.addMapping("/users/*")
            registry.addMapping("/**")
    //                .allowedOrigins("*")
                    .allowedOrigins("http://localhost:63343")
                    .allowCredentials(true) // 允许客户端发送Cookie
                    .allowedMethods("GET", "POST");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    项目中可以这样配置:

    @Component
    @Data
    @ConfigurationProperties("cors")
    public class CORSProperties {
        String pathPattern; // 允许哪些路径下的API被跨域访问
        String[] origins; // 允许跨域请求的源
        String[] methods; // 允许跨域请求的方法类型
        boolean allowCredentials; // 是否允许Cookie
    //    详细信息可以参考org.springframework.web.bind.annotation.CrossOrigin
    //    String[] allowedHeaders;
    //    String[] exposedHeaders;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    cors:
      path-pattern: /**
      methods:
        - GET
        - POST
      origins:
        - http://localhost:63343
        - http://192.168.152.130:8888
      allow-credentials: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        @Autowired
        private CORSProperties corsProperties;
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping(corsProperties.getPathPattern())
                    .allowedOrigins(corsProperties.getOrigins())
                    .allowCredentials(corsProperties.isAllowCredentials())
                    .allowedMethods(corsProperties.getMethods());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    客户端使用

    <button type="button" id="load-btn">加载用户信息button>
    
    <script src="./js/jquery.min.js">script>
    <script>
        $(() => {
            $('#load-btn').click(() => {
                $.getJSON('http://localhost:8080/jk/users', (users) => {
                    const $table = $('')$(document.body).append($table)for(const user of users){const $tr =$('')
                        $table.append($tr)
    
                        $tr.append(``)
                        $tr.append(``)}})})})script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    Layui

    • 官网:http://layuimini.99php.cn/
    • 文档:http://layuimini.99php.cn/docs/
    • 演示:http://layuimini.99php.cn/onepage/v2/index.html
    • Github:https://github.com/zhongshaofa/layuimini/tree/v2-onepage
    • 下载:https://codeload.github.com/zhongshaofa/layuimini/zip/refs/heads/v2-onepage

    在这里插入图片描述

    在这里插入图片描述

    注意:Layui中的相对路径都是相对于index.html来说的。

    注意:layuimini的表格要求服务器返回的JSON数据格式如下:

    {
      "code": 0,
      "data": [
        {
          "id": 1,
          "name": "职业",
          "value": "job",
          "intro": "一份工作"
        },
        {
          "id": 2,
          "name": "性格",
          "value": "character",
          "intro": "人的性格"
        }
      ],
      "count": 87
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    MySQL建议

    在这里插入图片描述

    在这里插入图片描述

    最大IP地址:255.255.255.255
    
    CREATE TABLE user(
    	age TINYINT UNSIGNED,
    	ip VARCHAR(15) #需要15个字节
    )
    
    CREATE TABLE user(
    	age TINYINT UNSIGNED,
    	ip INT UNSIGNED #只需要4个字节
    )
    
    INSERT INTO user VALUES(10, INET_ATON('255.255.255.255'))
    
    SELECT INET_NTOA(ip) FROM user
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    注意:

    • 索引只能给非空列进行优化

    • 不使用外键有可能导致数据的一致性出现问题,因此需要自己在Java代码的业务层做好业务逻辑控制

    • 数据的一致性远远没有数据库的性能重要

    • 高并发&分布式下的系统为了性能更优以及更好维护,不能使用外键。一般而言只要自己在应用层面做好数据库之间关系的维护,那么不使用外键是完全没有问题的。

    数据字典

    在这里插入图片描述

    在这里插入图片描述

    使用PowerDesigner:

    在这里插入图片描述

    /*==============================================================*/
    /* DBMS name:      MySQL 5.0                                    */
    /* Created on:     2022-05-05 23:16:43                          */
    /*==============================================================*/
    
    
    drop table if exists dict_item;
    
    drop table if exists dict_type;
    
    /*==============================================================*/
    /* Table: dict_item                                             */
    /*==============================================================*/
    create table dict_item
    (
       id                   bigint not null,
       name                 varchar(20) not null,
       value                varchar(20) not null,
       no                   int not null default 0 comment '用来排序,数字越小,优先级越高,越先展示',
       type_id              bigint comment '该条目所属的数据字典类型',
       status               int not null default 1 comment '是否启用该条目,0:不启用,1:启用',
       primary key (id),
       unique key AK_UK_1 (name, type_id),
       unique key AK_UK_2 (value, type_id)
    );
    
    alter table dict_item comment '数据字典每一项具体的内容';
    
    /*==============================================================*/
    /* Table: dict_type                                             */
    /*==============================================================*/
    create table dict_type
    (
       id                   bigint not null auto_increment,
       name                 varchar(20) not null comment '名称是展示在客户端的,是有可能会发生改变的',
       value                varchar(20) not null comment '值不会发生改变,编写SQL操作数据时,一般使用value而不是name',
       intro                varchar(100) comment '防止程序员忘记该数据字典类型的作用、功能(根据项目需求可有可无)',
       primary key (id),
       unique key AK_UK_1 (name),
       unique key AK_UK_2 (value)
    );
    
    alter table dict_type comment '数据字典类型';
    
    alter table dict_item add constraint FK_Reference_1 foreign key (type_id)
          references dict_type (id) on delete restrict on update restrict;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    使用IDEA自带的数据库工具+不使用外键+数据库使用最优字段类型:

    在这里插入图片描述

    create table jk.dict_type
    (
        id    smallint unsigned auto_increment comment '主键'
            primary key,
        name  varchar(20)  default '' not null comment '名称',
        value varchar(20)  default '' not null comment '值',
        intro varchar(100) default '' not null comment '简介',
        constraint dict_type_name_uindex
            unique (name),
        constraint dict_type_value_uindex
            unique (value)
    )
        comment '数据字典类型';
    
    create table jk.dict_item
    (
        id      smallint unsigned auto_increment comment '主键'
            primary key,
        name    varchar(20)       default '' not null comment '名称',
        value   varchar(20)       default '' not null comment '值',
        type_id smallint unsigned            not null comment '类型id',
        sn      smallint unsigned default 0  not null comment '排序序号:默认为0,值越大,越优先排列展示',
        # 其实个人认为enabled这个字段的类型可以设置为bool或者boolean
        enabled tinyint unsigned  default 1  not null comment '是否启用:0,禁用;1,启用;默认为1',
        constraint dict_item_name_type_id_uindex
            unique (name, type_id),
        constraint dict_item_value_type_id_uindex
            unique (value, type_id)
    )
        comment '数据字典条目';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    客户端:

    在这里插入图片描述

    在这里插入图片描述

    服务端:

    POJO—Query:

    @Data
    public class PageQuery {
        private static final int MIN_SIZE = 1;
        private static final int DEFAULT_SIZE = 10;
        private long size; // 一页展示多少条数据
        private long page; // 第几页
        /**
         * 查询出来的数据集
         * 由于将来查出来的类型不确定(VO、BO、PO),因此泛型使用类型通配符
         */
        private List<?> data;
        private long total; // 总条数
        private long pages; // 总页数
    
        public long getSize() {
            return size < MIN_SIZE ? DEFAULT_SIZE : size;
        }
    
        public long getPage() {
            return page < MIN_SIZE ? MIN_SIZE : page;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    @EqualsAndHashCode(callSuper = true)
    @Data
    public class KeywordQuery extends PageQuery {
        private String keyword;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    @EqualsAndHashCode(callSuper = true)
    @Data
    public class DictTypeQuery extends KeywordQuery {
    }
    
    • 1
    • 2
    • 3
    • 4

    ServiceImpl:

    @Service
    @Transactional
    public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {
        @Autowired
        private DictTypeMapper mapper;
    
        @Override
        @Transactional(readOnly = true)
        public void list(DictTypeQuery query) {
            final String keyword = query.getKeyword();
            LambdaQueryWrapper<DictType> wrapper = new LambdaQueryWrapper<>();
            // 按照关键字查询
            if (!StringUtils.isEmpty(keyword)) {
                wrapper.like(DictType::getName, keyword).or()
                        .like(DictType::getIntro, keyword).or()
                        .like(DictType::getValue, keyword);
            }
            // 按照id降序排序
            wrapper.orderByDesc(DictType::getId);
            // 分页查询
            Page<DictType> page = new Page<>(query.getNo(), query.getSize());
            mapper.selectPage(page, wrapper);
            // 更新query对象
            query.setData(page.getRecords());
            query.setPages(page.getPages());
            query.setTotal(page.getTotal());
            // 如果客户端的查询条件有问题,MyBatisPlus会自动识别并修正,因此可以修改一下query中的查询条件
            query.setSize(getSize());
            query.setPage(getCurrent());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    Controller:

    @RestController
    @RequestMapping("/dictTypes")
    public class DictTypeController {
        @Autowired
        private DictTypeService service;
    
        @GetMapping
        public Map<String, Object> list(DictTypeQuery query) {
            service.list(query);
            final Map<String, Object> map = new HashMap<>();
            map.put("msg", "");
            map.put("data", query.getData());
            map.put("count", query.getTotal());
            map.put("code", 0);
            return map;
        }
    
        @PostMapping("/remove")
        public Map<String, Object> remove(String id) {
            // id = "10"
            // id = "1, 20, 23"
            final String[] ids = id.split(",");
            final Map<String, Object> map = new HashMap<>();
            if (service.removeByIds(Arrays.asList(ids))) {
                map.put("msg", "删除成功");
                map.put("code", 0);
            } else {
                map.put("msg", "删除失败");
                map.put("code", 1);
            }
            return map;
        }
    
        @PostMapping("/save")
        public Map<String, Object> save(DictType dictType) {
            final Map<String, Object> map = new HashMap<>();
            if (service.saveOrUpdate(dictType)) {
                map.put("msg", "保存成功");
                map.put("code", 0);
            } else {
                map.put("msg", "保存失败");
                map.put("code", 1);
            }
            return map;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    封装MyBatis-Plus方便查询

    以查询DictType(数据字典类型)为例

    enhance—MPPage、MPQueryWrapper:

    public class MPPage<T> extends Page<T> {
        private final PageQuery query;
    
        public MPPage(PageQuery query) {
            super(query.getPage(), query.getSize());
            this.query = query;
        }
    
        public void updateQuery() {
            query.setData(getRecords());
            query.setPages(getPages());
            query.setTotal(getTotal());
            // 如果客户端的查询条件有问题,MyBatis会自动识别并修正,因此可以修改一下query中的查询数据
            query.setSize(getSize());
            query.setPage(getCurrent());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    public class MPQueryWrapper<T> extends LambdaQueryWrapper<T> {
        @SafeVarargs
        public final MPQueryWrapper<T> like(Object val, SFunction<T, ?>... funcs) {
            if (val == null || funcs == null || funcs.length == 0) return this;
            final String str = val.toString();
            if (str.length() == 0) return this;
    
            return (MPQueryWrapper<T>) nested((wrapper) -> {
                for (SFunction<T, ?> func : funcs) {
                    wrapper.like(func, str).or();
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    动态代理更新Query对象:

    @Configuration
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
    • 1
    • 2
    • 3
    • 4
    @Aspect
    @Component
    public class PageMapperInterceptor {
        @Around("execution(public com.baomidou.mybatisplus.core.metadata.IPage com.baomidou.mybatisplus.core.mapper.BaseMapper.selectPage(com.baomidou.mybatisplus.core.metadata.IPage, com.baomidou.mybatisplus.core.conditions.Wrapper))")
        public Object updateQuery(ProceedingJoinPoint point) throws Throwable {
            Object result = point.proceed();
            final Object[] args = point.getArgs();
            if (args != null && args.length > 0) {
                Object arg = args[0];
                if (arg instanceof MPPage) {
                    ((MPPage<?>) arg).updateQuery();
                }
            }
            return result;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    使用—ServiceImpl:

    @Transactional
    @Service
    public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {
        @Override
        @Transactional(readOnly = true)
        public void list(DictTypeQuery query) {
            MPQueryWrapper<DictType> wrapper = new MPQueryWrapper<>();
            wrapper.like(query.getKeyword(), DictType::getName, DictType::getValue, DictType::getIntro);
            wrapper.orderByDesc(DictType::getId);
            baseMapper.selectPage(new MPPage<>(query), wrapper);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Controller:

    @RestController
    @RequestMapping("/dictTypes")
    public class DictTypeController {
        @Autowired
        private DictTypeService service;
    
        @GetMapping
        public Map<String, Object> list(DictTypeQuery query) {
            service.list(query);
            final Map<String, Object> map = new HashMap<>();
            map.put("msg", "");
            map.put("data", query.getData());
            map.put("count", query.getTotal());
            map.put("code", 0);
            return map;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    封装给客户端的返回值

    public class R extends HashMap<String, Object> {
        public static final int CODE_SUCCESS = 0;
        
        private static final String K_CODE = "code";
        private static final String K_MSG = "msg";
        private static final String K_DATA = "data";
    
        public R setCode(int code) {
            return add(K_CODE, code);
        }
    
        public R setMsg(String msg) {
            return add(K_MSG, msg);
        }
    
        public R setData(Object data) {
            return add(K_DATA, data);
        }
    
        public R add(String key, Object data) {
            put(key, data);
            return this;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    public final class Rs {
        private Rs() {
        }
    
        // 事先和前端约定好:成功code:0,失败code:1
        public static final int CODE_SUCCESS = 0;
        public static final int CODE_ERROR_DEFAULT = 1;
    
        private static R success() {
            return new R().setCode(CODE_SUCCESS);
        }
    
        public static R success(PageQuery query) {
            return success().setData(query.getData());
        }
    
        public static R success(String msg) {
            return success().setMsg(msg);
        }
    
        public static R success(PageQuery query, String msg) {
            return success().setData(query.getData()).setMsg(msg);
        }
    
        public static R error() {
            return new R().setCode(CODE_ERROR_DEFAULT);
        }
    
        public static R error(String msg) {
            return error().setMsg(msg);
        }
    
        public static R error(int code, String msg) {
            return new R().setCode(code).setMsg(msg);
        }
    
        public static R r(boolean success) {
            return new R().setCode(success ? CODE_SUCCESS : CODE_ERROR_DEFAULT);
        }
    
        public static R r(boolean success, String msg) {
            return r(success).setMsg(msg);
        }
    
        public static R r(boolean success, Object data) {
            return r(success).setData(data);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    Controller使用:

    @RestController
    @RequestMapping("/dictTypes")
    public class DictTypeController {
        @Autowired
        private DictTypeService service;
    
        @GetMapping
        public R list(DictTypeQuery query) {
            service.list(query);
            return Rs.success(query).add("count", query.getTotal());
        }
    
        @PostMapping("/remove")
        public R remove(String id) {
            // id = "10"
            // id = "1, 20, 23"
            final String[] ids = id.split(",");
    
    //        if (service.removeByIds(Arrays.asList(ids))) {
    //            return Rs.success("删除成功");
    //        } else {
    //            return Rs.error("删除失败");
    //        }
    
            final boolean success = service.removeByIds(Arrays.asList(ids));
            final String msg = success ? "删除成功" : "删除失败";
            return Rs.r(success, msg);
        }
    
        @PostMapping("/save")
        public R save(DictType dictType) {
            if (!service.saveOrUpdate(dictType)) {
                throw new RuntimeException("保存失败");
            }
            return Rs.success("保存成功");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    统一异常处理+HTTP响应状态码

    • 如果服务器端操作失败的话,比如删除失败、保存失败,那么给客户端返回的StatusCode就不应该是200,应该是400/500,原因如下:

    • 客户端(前端)极大可能是根据HTTP请求的响应状态码来判断某个请求是否成功的,而不是通过服务器返回的JSON数据的某个属性值来判断

    • 比如AJAX的回调方法默认就是通过HTTP的响应状态码来判断是否请求成功的。

    • 因此如果服务器处理数据失败,应该修改响应状态码200(OK)为其它StatusCode,比如400、500。

      public interface JSONable {
          default String jsonString() throws Exception {
              return JSONs.getMAPPER().writeValueAsString(this);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      public class R extends HashMap<String, Object> implements JSONable {
      	// ...
      }
      
      • 1
      • 2
      • 3
      @ControllerAdvice
      public class ExceptionInterceptor {
          // 默认处理所有的异常
          @ExceptionHandler(Throwable.class)
          public void exceptionHandlerOther(Throwable throwable, HttpServletResponse response) throws Exception {
              response.setCharacterEncoding("UTF-8");
              response.setStatus(400);
      //        response.getWriter().write(Rs.error(getRealCause(throwable).getMessage()).jsonString());
              response.getWriter().write(Rs.error(throwable.getMessage()).jsonString());
          }
      
          private Throwable getRealCause(Throwable throwable) {
              Throwable cause = throwable.getCause();
              while (cause != null) {
                  throwable = cause;
                  cause = cause.getCause();
              }
              return throwable;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      @RestController
      @RequestMapping("/dictTypes")
      public class DictTypeController {
          @Autowired
          private DictTypeService service;
      
          @PostMapping("/remove")
          public R remove(String id) {
              final String[] ids = id.split(",");
              if (!service.removeByIds(Arrays.asList(ids))) {
                  throw new RuntimeException("删除失败");
              }
              return Rs.success("删除成功");
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    • 还可以继续封装:

      public enum CodeMsg {
          BAD_REQUEST(400, "请求出错"),
          UNAUTHORIZED(401, "未授权"),
          FORBIDDEN(403, "禁止访问"),
          NOT_FOUND(404, "资源不存在"),
          INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
      
          OPERATE_OK(R.CODE_SUCCESS, "操作成功"),
          SAVE_OK(R.CODE_SUCCESS, "保存成功"),
          REMOVE_OK(R.CODE_SUCCESS, "删除成功"),
      
          OPERATE_ERROR(40001, "操作失败"),
          SAVE_ERROR(40002, "保存失败"),
          REMOVE_ERROR(40003, "删除失败"),
          UPLOAD_IMG_ERROR(40004, "图片上传失败"),
      
          WRONG_USERNAME(50001, "用户名不存在"),
          WRONG_PASSWORD(50002, "密码错误"),
          USER_LOCKED(50003, "用户被锁定,无法正常登录"),
          WRONG_CAPTCHA(50004, "验证码错误"),
      
          NO_TOKEN(60001, "没有Token,请登录"),
          TOKEN_EXPIRED(60002, "Token过期,请重新登录"),
          NO_PERMISSION(60003, "没有相关的操作权限");
      
          private final int code;
          private final String msg;
      
          CodeMsg(int code, String msg) {
              this.code = code;
              this.msg = msg;
          }
      
          public int getCode() {
              return code;
          }
      
          public String getMsg() {
              return msg;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      @EqualsAndHashCode(callSuper = true)
      @Data
      public class CommonException extends RuntimeException {
          private int code;
      
          public CommonException() {
              this(CodeMsg.BAD_REQUEST.getCode(), null);
          }
      
          public CommonException(String msg) {
              this(msg, null);
          }
      
          public CommonException(int code, String msg) {
              this(code, msg, null);
          }
      
          public CommonException(String msg, Throwable cause) {
              this(CodeMsg.BAD_REQUEST.getCode(), msg, cause);
          }
      
          public CommonException(int code, String msg, Throwable cause) {
              super(msg, cause);
              this.code = code;
          }
      
          public CommonException(CodeMsg codeMsg) {
              this(codeMsg, null);
          }
      
          public CommonException(CodeMsg codeMsg, Throwable cause) {
              this(codeMsg.getCode(), codeMsg.getMsg(), cause);
          }
      
          public int getCode() {
              return code;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      public final class Rs {
          private Rs() {
          }
      
          public static final String K_COUNT = "count";
      
          private static final int CODE_SUCCESS = 0;
          private static final int CODE_ERROR_DEFAULT = CodeMsg.BAD_REQUEST.getCode();
      
          private static R success() {
              return new R().setCode(CODE_SUCCESS);
          }
      
          public static R success(PageQuery query) {
              return success().setData(query.getData());
          }
      
          public static R success(String msg) {
              return success().setMsg(msg);
          }
      
          public static R success(Object data) {
              return success().setData(data);
          }
      
          public static R success(PageQuery query, String msg) {
              return success().setData(query.getData()).setMsg(msg);
          }
      
          public static R success(CodeMsg codeMsg) {
              return success().setMsg(codeMsg.getMsg());
          }
      
          public static R error() {
              return error(CODE_ERROR_DEFAULT);
          }
      
          public static R error(int code) {
              return new R().setCode(code);
          }
      
          public static R error(String msg) {
              return error().setMsg(msg);
          }
      
          public static R error(int code, String msg) {
              return error(code).setMsg(msg);
          }
      
          public static R error(Throwable e) {
      //        R r = error(e.getMessage()); // 开发阶段
              R r = error(); // 项目上线 项目上线了就不要把其它异常信息给用户看了
              if (e instanceof CommonException) {
                  r.setCode(((CommonException) e).getCode());
              }
              return r;
          }
      
          public static R r(boolean success) {
              return new R().setCode(success ? CODE_SUCCESS : CODE_ERROR_DEFAULT);
          }
      
          public static R r(boolean success, String msg) {
              return r(success).setMsg(msg);
          }
      
          public static R r(boolean success, Object data) {
              return r(success).setData(data);
          }
          
          public static R exception(String msg) {
              throw new CommonException(msg);
          }
      
          public static R exception(CodeMsg codeMsg) {
              throw new CommonException(codeMsg);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      @ControllerAdvice
      public class ExceptionInterceptor {
          @ExceptionHandler(Throwable.class)
          public void handle(Throwable throwable,
                             HttpServletResponse response) throws Exception {
      //        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE/*"application/json; charset=UTF-8"*/);
              response.setContentType(MediaType.APPLICATION_JSON_VALUE);
              response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
              response.setStatus(400);
      //        response.getWriter().write(Rs.error(getRealCause(throwable)).json());
              response.getWriter().write(Rs.error(throwable).json());
          }
      
          private Throwable getRealCause(Throwable throwable) {
              Throwable cause = throwable.getCause();
              while (cause != null) {
                  throwable = cause;
                  cause = cause.getCause();
              }
              return throwable;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    • 其实异常处理器还有更简便的写法:
      在这里插入图片描述

      @RestControllerAdvice
      @Slf4j
      public class CommonExceptionHandler {
          @ExceptionHandler(Throwable.class)
          @ResponseStatus(code = HttpStatus.BAD_REQUEST)
          public JSONResult handle(Throwable throwable) {
      //        System.out.println(throwable);
              log.error("error", throwable);
              return JSONResults.exception(throwable);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • Controller使用:

      @RestController
      @RequestMapping("/dictTypes")
      public class DictTypeController {
          @Autowired
          private DictTypeService service;
      
          @GetMapping
          public R list(DictTypeQuery query) {
              service.list(query);
              return Rs.success(query).add(Rs.K_COUNT, query.getTotal());
          }
      
          @PostMapping("/remove")
          public R remove(String id) {
              // id = "10"
              // id = "1, 20, 23"
              final String[] ids = id.split(",");
              if (!service.removeByIds(Arrays.asList(ids))) {
                  throw new CommonException(CodeMsg.REMOVE_ERROR);
              }
              return Rs.success(CodeMsg.REMOVE_OK);
          }
      
          @PostMapping("/save")
          public R save(DictType dictType) {
              if (!service.saveOrUpdate(dictType)) {
                  throw new CommonException(CodeMsg.SAVE_ERROR);
              }
              return Rs.success(CodeMsg.SAVE_OK.getMsg());
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
    • 还可以将Controller中的公共代码抽取出来:

      public abstract class BaseController<T> {
          protected abstract IService<T> service();
      
          @GetMapping("/list")
          public R list() {
              return Rs.success(service().list());
          }
          
          @PostMapping("/remove")
          public R remove(String id) {
              final String[] ids = id.split(",");
              if (!service().removeByIds(Arrays.asList(ids))) {
                  Rs.exception(CodeMsg.REMOVE_ERROR);
              }
              // return Rs.success(CodeMsg.REMOVE_OK.getMsg());
              return Rs.success(CodeMsg.REMOVE_OK);
          }
      
          @PostMapping("/save")
          public R save(T entity) {
              if (!service().saveOrUpdate(entity)) {
                  // throw new CommonException(CodeMsg.SAVE_ERROR);
                  Rs.exception(CodeMsg.SAVE_ERROR);
              }
              return Rs.success(CodeMsg.SAVE_OK);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      @RestController
      @RequestMapping("/dictTypes")
      public class DictTypeController extends BaseController<DictType> {
          @Autowired
          private DictTypeService service;
      
          @GetMapping
          public R list(DictTypeQuery query) {
              service.list(query);
              return Rs.success(query).add(Rs.K_COUNT, query.getTotal());
          }
      
          @Override
          protected IService<DictType> service() {
              return service;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

    统一异常处理—配合Shiro

    @RestControllerAdvice只能拦截到Controller抛出的异常

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    public class ErrorFilter implements Filter {
        public static final String ERROR_URI = "/handleError";
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            try {
                chain.doFilter(request, response);
            } catch (Exception e) {
                request.setAttribute(ERROR_URI, e);
                request.getRequestDispatcher(ERROR_URI).forward(request, response);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    @Configuration
    public class SpringMVCConfig implements WebMvcConfigurer {
        @Bean
        public FilterRegistrationBean<Filter> filterRegistrationBean() {
            FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
            // 设置Filter
            bean.setFilter(new ErrorFilter());
            bean.addUrlPatterns("/*");
            // 最高权限
            bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
            return bean;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    @RestController
    public class ErrorController {
        @RequestMapping(ErrorFilter.ERROR_URI)
        public void handle(HttpServletRequest request) throws Exception {
            // 抛出异常
            throw (Exception) request.getAttribute(ErrorFilter.ERROR_URI);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    @RestControllerAdvice
    @Slf4j
    public class CommonExceptionHandler {
        @ExceptionHandler(Throwable.class)
        @ResponseStatus(code = HttpStatus.BAD_REQUEST)
        public JSONResult handle(Throwable t) {
            log.error("handle", t);
    
            // 一些可以直接处理的异常
            if (t instanceof CommonException) {
                return handle((CommonException) t);
            } else if (t instanceof BindException) {
                return handle((BindException) t);
            } else if (t instanceof ConstraintViolationException) {
                return handle((ConstraintViolationException) t);
            } else if (t instanceof AuthorizationException) {
                return JSONResults.error(CodeMsg.NO_PERMISSION);
            }
    
            // 处理cause异常(导致产生t的异常)
            Throwable cause = t.getCause();
            if (cause != null) {
                return handle(cause);
            }
    
            // 其他异常(没有cause的异常)
            return JSONResults.error();
        }
    
        private JSONResult handle(CommonException ce) {
            return JSONResults.error(ce.getCode(), ce.getMessage());
        }
    
        private JSONResult handle(BindException be) {
            List<ObjectError> errors = be.getBindingResult().getAllErrors();
            // 函数式编程的方式:stream
            List<String> defaultMsgs = Streams.map(errors, ObjectError::getDefaultMessage);
            String msg = StringUtils.collectionToDelimitedString(defaultMsgs, ", ");
            return JSONResults.error(msg);
        }
    
        private JSONResult handle(ConstraintViolationException cve) {
            List<String> msgs = Streams.map(cve.getConstraintViolations(), ConstraintViolation::getMessage);
            String msg = StringUtils.collectionToDelimitedString(msgs, ", ");
            return JSONResults.error(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    @Configuration
    public class ShiroConfig {
        /**
         * ShiroFilterFactoryBean用来告诉Shiro如何进行拦截
         * 1.拦截哪些URL
         * 2.每个URL需要经过哪些filter
         */
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(Realm realm/*, JkProperties properties*/) {
            ShiroFilterFactoryBean filterBean = new ShiroFilterFactoryBean();
    
            // 安全管理器
            filterBean.setSecurityManager(new DefaultWebSecurityManager(realm));
    
            // 添加一些自定义Filter
            Map<String, Filter> filters = new HashMap<>();
            filters.put("token", new TokenFilter());
            filterBean.setFilters(filters);
    
            // 设置URL如何拦截
            // Filter的顺序很重要,因此需要使用LinkedHashMap
            Map<String, String> urlMap = new LinkedHashMap<>();
    
    		// ...
    
            // 放行全局Filter的异常处理
            urlMap.put(ErrorFilter.ERROR_URI, "anon");
    
            // 其他
            urlMap.put("/**", "token");
            filterBean.setFilterChainDefinitionMap(urlMap);
    
            return filterBean;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    数据的一致性

    外键有优点,同样,也有它的缺点

    • 如果在项目中使用了外键,那么表与表之间的数据一致性其实是不用我们操心的,因为有外键自动帮我们约束。
    • 因此对于那些小型的、对于数据一致性要求很高的项目,需要使用外键。
    • 但是大型的、分布式的互联网项目出于对数据库的性能、备份、迁移、维护等原因的考虑,一般而言在设计数据库时是不使用外键的,那么这时表与表之间的联系,也就是数据一致性问题怎么解决呢?
    • 答案就是需要我们自己在应用层做好相应的处理,但做好这个处理并不简单。

    假设现在要对某张表的进行数据一致性的处理,有许多非常麻烦的点

    • 虽然数据库没有使用外键,但对业务来讲,表与表之间应该是有联系的
    • 那么对这张表进行删除、更新、添加等操作都需要考虑数据一致性,具体到代码,就是remove、save、update这种方法有很多,需要都考虑到
    • 有可能会有很多表在数据上都关联这张表,因此我们需要清楚每一张表与每一张表之间的关联关系,知道了表与表之间的关联关系后,才能去逐个处理

    解决方案:自己写一个保证数据一致性的小框架,这个小框架的特点:

    • 注解驱动
    • AOP
    • 反射

    MJ老师编写框架经验:

    • 如果你自己想写一个比较好用的框架
    • 首先应该从应用的角度出发,先从使用者(自己、其他开发者)的角度出发
    • 考虑别人应该怎么用这个框架、这个框架能够怎样简化开发、这个框架怎么样能够使开发变得更爽、更高效、更敏捷
    • 然后再考虑减少BUG
    • 然后再考虑安全问题
    • 然后再考虑性能问题
    • 然后再考虑解耦、抽取、可扩展…

    拼音库—tinypinyin的使用

    	
    	<dependency>
    	    <groupId>com.github.promeggroupId>
    	    <artifactId>tinypinyinartifactId>
    	    <version>2.0.3version>
    	dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
        // IService.saveOrUpdate方法里面会调用updateById或者save方法,因此需要重写这两个方法对拼音进行处理
        @Override
        public boolean updateById(PlateRegion entity) {
            processPinyin(entity);
            return super.updateById(entity);
        }
        @Override
        public boolean save(PlateRegion entity) {
            processPinyin(entity);
            return super.save(entity);
        }
        private void processPinyin(PlateRegion entity) {
            final String name = entity.getName();
            if (StringUtils.isEmpty(name)) return;
            entity.setPinyin(Pinyin.toPinyin(name, "_"));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    MapStruct

    作用:对象转换

    • po ——> vo
    • vo ——> po
    	<dependency>
    	    <groupId>org.mapstructgroupId>
    	    <artifactId>mapstructartifactId>
    	    <version>${map.struct.version}version>
    	    <scope>providedscope>
    	dependency>
    	<dependency>
    	    <groupId>org.mapstructgroupId>
    	    <artifactId>mapstruct-processorartifactId>
    	    <version>${map.struct.version}version>
    	    <scope>providedscope>
    	dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    @Mapper
    public interface MapStruct {
        MapStruct INSTANCE = Mappers.getMapper(MapStruct.class);
    
        DictItem vo2po(ReqSaveDictItem reqSaveVo);
        DictType vo2po(ReqSaveDictType reqSaveVo);
        ExamPlace vo2po(ReqSaveExamPlace reqSaveVo);
        ExamPlaceCourse vo2po(ReqSaveExamPlaceCourse reqSaveVo);
        PlateRegion vo2po(ReqSavePlateRegion reqSaveVo);
    
        RespDictItem po2vo(DictItem po);
        RespDictType po2vo(DictType po);
        RespExamPlace po2vo(ExamPlace po);
        RespExamPlaceCourse po2vo(ExamPlaceCourse po);
        RespPlateRegion po2vo(PlateRegion po);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    基本使用:

    	final DictItem dictItem = MapStruct.INSTANCE.vo2po(new ReqSaveDictItem());
    	final RespDictItem respDictItem = MapStruct.INSTANCE.po2vo(new DictItem());
    
    • 1
    • 2

    项目中使用:

    public abstract class BaseController<T, ReqSave> {
        protected abstract Function<ReqSave, T> function();
        // ...
        @PostMapping("/save")
        public JSONResult save(@Valid ReqSave entity) {
            service.saveOrUpdate(function().apply(entity));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    public class DictItemController extends BaseController<DictItem, ReqSaveDictItem> {
        // ...
        @Override
        protected Function<ReqSaveDictItem, DictItem> function() {
            return MapStruct.INSTANCE::vo2po;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    public class PlateRegionServiceImpl extends ServiceImpl<PlateRegionMapper, PlateRegion> implements PlateRegionService {
        public JSONDataResult<List<RespPlateRegion>> listProvinces() {
    	    // ...
            final List<RespPlateRegion> data = baseMapper.selectList(wrapper)
                    .stream().map(MapStruct.INSTANCE::po2vo)
                    .collect(Collectors.toList());
            return JSONResults.success(data);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    自定义转换规则

    public class MapStructFormatter {
        @Qualifier
        @Target(ElementType.METHOD)
        @Retention(RetentionPolicy.CLASS)
        public @interface Date2Millis {}
    
        @Date2Millis
        public static Long date2millis(Date date) {
            if (date == null) return null;
            return date.getTime();
        }
    
    	/*
        @Qualifier
        @Target(ElementType.METHOD)
        @Retention(RetentionPolicy.CLASS)
        public @interface Mills2Date {}
    
        @Mills2Date
        public static Date millis2date(Long mills) {
            if (mills == null) return null;
            return new Date(mills);
        }
        */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    @Data
    public class SysUser {
    	// ...
        //最后一次登录的时间
        private Date loginTime;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    @Data
    @ApiModel("系统用户")
    public class RespSysUser {
    	// ...
    	
        @ApiModelProperty("最后一次登录的时间")
        // 前后端分离一般返回UNIX时间戳
        // UNIX时间戳:从 1970-1-1 0:0:0 开始到现在走过的毫秒数
        private Long loginTime;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    /**
     * ReqVo -> Po
     * Po -> Vo
     */
    @Mapper(uses = {
            MapStructFormatter.class
    })
    public interface MapStructs {
    	// ...
        @Mapping(source = "loginTime",
                target = "loginTime",
                qualifiedBy = MapStructFormatter.Date2Millis.class)
        RespSysUser po2vo(SysUser po);
    	// ...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    登录—简单登录

    	
    	<dependency>
    	    <groupId>com.github.whvcsegroupId>
    	    <artifactId>easy-captchaartifactId>
    	    <version>1.6.2version>
    	dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    @RestController
    @RequestMapping("/sysUsers")
    @Api(tags = "系统用户", description = "SysUser")
    public class SysUserController extends BaseController<SysUser, ReqSaveSysUser> {
        @Autowired
        private SysUserService service;
    
        @GetMapping("/captcha")
        @ApiOperation("生成验证码")
        public void captcha(HttpServletRequest request,
                            HttpServletResponse response) throws Exception {
            CaptchaUtil.out(request, response);
        }
    
        @PostMapping("/login")
        @ApiOperation("登录")
        public JSONDataResult<RespLogin> login(ReqLogin reqVo, HttpServletRequest request) {
            if (CaptchaUtil.ver(reqVo.getCaptcha(), request)) {
                return JSONResults.success(service.login(reqVo));
            }
            JSONResults.exception(CodeMsg.WRONG_CAPTCHA);
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    	@Override
    	public RespLogin login(ReqLogin reqVo) {
    	    // 根据用户名查询用户
    	    MPLambdaQueryWrapper<SysUser> wrapper = new MPLambdaQueryWrapper<>();
    	    wrapper.eq(SysUser::getUsername, reqVo.getUsername());
    	    SysUser po = baseMapper.selectOne(wrapper);
    	
    	    // 用户名不存在
    	    if (po == null) {
    	        return JsonVos.raise(CodeMsg.WRONG_USERNAME);
    	    }
    	
    	    // 密码不正确
    	    if (!po.getPassword().equals(reqVo.getPassword())) {
    	        return JsonVos.raise(CodeMsg.WRONG_PASSWORD);
    	    }
    	
    	    // 账号锁定
    	    if (po.getStatus() == Constants.SysUserStatus.LOCKED) {
    	        return JsonVos.raise(CodeMsg.USER_LOCKED);
    	    }
    
    		// 登录成功
    	
    	    // 更新登录时间
    	    po.setLoginTime(new Date());
    	    baseMapper.updateById(po);
    	
    	    // 返回给客户端的具体数据
    	    RespLogin vo = MapStruct.INSTANCE.po2loginVo(po);
    	    return vo;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    前端Ajax登录:

    Ajaxs.loadPost({
        uri: 'sysUsers/login',
        data: data.field,
        success: (response) => {
            location.href = '../index.html'
        },
        xhrFields: { // 需要跨域带上cookie
            withCredentials: true
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    登录—Token

    后端

    • 登录

      	@Data
      	@ApiModel("登录成功的结果")
      	public class RespLogin {
      		// ...
      	    @ApiModelProperty("登录令牌")
      	    private String token;
      	}
      
          @PostMapping("/login")
          @ApiOperation("登录")
          public JSONDataResult<RespLogin> login(ReqLogin reqVo, HttpServletRequest request) {
              if (CaptchaUtil.ver(reqVo.getCaptcha(), request)) {
                  return JSONResults.success(service.login(reqVo));
              }
              JSONResults.exception(CodeMsg.WRONG_CAPTCHA);
              return null;
          }
      
          @Override
          public RespLogin login(ReqLogin reqVo) {
              // 根据用户名查询用户
              MPLambdaQueryWrapper<SysUser> wrapper = new MPLambdaQueryWrapper<>();
              wrapper.eq(SysUser::getUsername, reqVo.getUsername());
              SysUser po = baseMapper.selectOne(wrapper);
      
              // 用户名不存在
              if (po == null) {
                  return JsonVos.raise(CodeMsg.WRONG_USERNAME);
              }
              // 密码不正确
              if (!po.getPassword().equals(reqVo.getPassword())) {
                  return JsonVos.raise(CodeMsg.WRONG_PASSWORD);
              }
              // 账号锁定
              if (po.getStatus() == Constants.SysUserStatus.LOCKED) {
                  return JsonVos.raise(CodeMsg.USER_LOCKED);
              }
      
              /**** 登录成功 ****/
              
              // 更新登录时间
              po.setLoginTime(new Date());
              baseMapper.updateById(po);
      
              // 生成Token,发送Token给用户
              String token = UUID.randomUUID().toString();
              // 存储token到缓存中
              Caches.putToken(token, po);
              
              // 返回给客户端的具体数据
              RespLogin vo = MapStruct.INSTANCE.po2loginVo(po);
              vo.setToken(token);
              return vo;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
    • 退出登录

          @PostMapping("/logout")
          @ApiOperation("退出登录")
          public JSONResult logout(@RequestHeader("Token") String token) {
              Caches.removeToken(token);
              return JSONResults.success(CodeMsg.LOGOUT_OK);
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

    前端

    • 登录:

      class DataKey {
          static USER = 'user'
          static TOKEN = 'token'
          static TOKEN_HEADER = 'Token'
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      	Ajaxs.loadPost({
      	    uri: 'sysUsers/login',
      	    data: data.field,
      	    success: (response) => {
      	        Datas.save(DataKey.USER, response.data)
      	        location.href = '../index.html'
      	    },
      	    xhrFields: { // 需要跨域带上cookie
      	        withCredentials: true
      	    }
      	})
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 需要确保登录后的每次请求都带上Token信息

          static get() { // Datas.get
              let ret = layui.data(this.TABLE)
              for (let i = 0; i < arguments.length; i++) {
                  if (!ret) return null
                  ret = ret[arguments[i]]
              }
              return ret
          }
          
          static _addTokenHeader(cfg) {
              // 取出token
              const token = Datas.get(DataKey.USER, DataKey.TOKEN)
              if (token) {
                  if (!cfg.headers) {
                      cfg.headers = {}
                  }
                  // 将token放到请求头
                  cfg.headers[DataKey.TOKEN_HEADER] = token
              }
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      	// 自己封装的Ajax请求,每一次请求都需要带上Token信息
          static ajax(cfg) {
              cfg.url = Commons.url(cfg.uri)
              Commons._addTokenHeader(cfg)
              $.ajax(cfg)
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      	// Layui发送请求时也需要带上Token信息
          _init() {
              const cfg = this._commonCfg()
              cfg.url = Commons.url(this._cfg.uri)
              $.extend(cfg, this._cfg)
              cfg.elem = cfg.selector
      
              Commons._addTokenHeader(cfg)
      
              this._innerTable = this._layuiTable().render(cfg)
              this._cfg = cfg
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 登出:

      	$('.login-out').click(() => {
      	    // 发送请求给服务器:退出登录
      	    Ajaxs.loadPost({
      	        uri: 'sysUsers/logout',
      	        success: () => {
      	            // 清除客户端缓存
      	            Datas.remove(DataKey.USER)
      	            // 提示
      	            Layers.msgSuccess('退出登录成功', () => {
      	                location.href = 'page/login.html'
      	            })
      	        }
      	    })
      	})
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

    权限管理—RBAC

    可以登录后台管理系统的员工/系统管理员:比如:sys_user(表名以sys_开头)

    使用产品的用户/客户(APP、小程序、网页):比如:user

    在这里插入图片描述

    在这里插入图片描述

    表结构设计

    可以登录后台管理系统的员工/系统管理员:比如:sys_user(表名以sys_开头)

    使用产品的用户/客户(APP、小程序、网页):比如:user

    create table if not exists jk.sys_resource
    (
    	id tinyint unsigned auto_increment comment '主键'
    		primary key,
    	name varchar(15) default '' not null comment '名称',
    	uri varchar(100) default '' not null comment '链接地址',
    	permission varchar(100) default '' not null comment '权限标识',
    	type tinyint unsigned default 0 not null comment '资源类型(0是目录,1是菜单,2是按钮)PS:按钮就是增删改查之类的能点击的',
    	icon varchar(100) default '' not null comment '图标',
    	sn tinyint unsigned default 0 not null comment '序号',
    	parent_id tinyint unsigned default 0 not null comment '父资源id',
    	constraint sys_resource_parent_id_name_uindex
    		unique (parent_id, name)
    )
    comment '资源';
    
    create table if not exists jk.sys_role
    (
    	id tinyint unsigned auto_increment comment '主键'
    		primary key,
    	name varchar(15) default '' not null comment '角色名称',
    	constraint sys_role_name_uindex
    		unique (name)
    )
    comment '角色';
    
    create table if not exists jk.sys_role_resource
    (
    	role_id tinyint unsigned default 0 not null comment '角色id',
    	resource_id tinyint unsigned default 0 not null comment '资源id',
    	primary key (resource_id, role_id)
    )
    comment '角色-资源';
    
    create table if not exists jk.sys_user
    (
    	id smallint unsigned auto_increment comment '主键'
    		primary key,
    	nickname varchar(15) default '' not null comment '昵称',
    	username varchar(15) default '' not null comment '登录用的用户名',
    	password char(32) default '' not null comment '登录用的密码,密码经过MD5加密之后就是32位的字符串',
    	create_time datetime default CURRENT_TIMESTAMP not null comment '创建的时间',
    	login_time datetime null comment '最后一次登录的时间',
    	status tinyint unsigned default 0 not null comment '账号的状态,0是正常,1是锁定',
    	constraint sys_user_username_uindex
    		unique (username)
    )
    comment '用户(可以登录后台系统的)';
    
    create table if not exists jk.sys_user_role
    (
    	role_id tinyint unsigned default 0 not null comment '角色id',
    	user_id smallint unsigned default 0 not null comment '用户id',
    	primary key (user_id, role_id)
    )
    comment '用户-角色';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    逻辑删除

    • 物理删除:真正从数据库中删除了,永久消失。

    • 逻辑删除(假删除、软删除):数据还在数据库中,只是对用户来说,数据被删掉了。

    • 逻辑删除的实现:在需要实现逻辑删除的表中增加一个字段来标识数据是否被删除。

    在这里插入图片描述

    逻辑删除—MyBatisPlus

    在这里插入图片描述

    # db: test
    create table user
    (
    	id int unsigned auto_increment
    		primary key,
    	name varchar(15) default '' not null,
    	deleted tinyint unsigned default 0 not null comment '1是被删除,0是未删除',
    	constraint user_name_uindex
    		unique (name)
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    企业级文件上传

    • 方案一:文件数据和表单数据一起提交(之前Java②学过的那种)

    • 方案二:专门弄一个文件服务器,用来操作文件(文件上传、下载、删除等)。文件数据先单独提交,从文件服务器返回一个uri,拿着这个uri和表单数据一起提交【前端需要进行用户的行为控制,比较复杂一点】在这里插入图片描述

    @RequestBody修饰的请求参数

    1. 前端/客户端将Content-Type改为:application/json

    2. 请求体传递符合要求的JSON字符串【JSON优势:灵活、前端处理方便、第三方库也多】

          @PostMapping("/save1")
          @ApiOperation("添加或更新")
          public JsonVo save1(User user) { // 相当于是加了@RequestParam
              JsonVo jsonVo = new JsonVo();
              jsonVo.setMsg(service.saveOrUpdate(user) ? "保存成功" : "保存失败");
              return jsonVo;
          }
          
          @PostMapping("/save2")
          @ApiOperation("添加或更新")
          public JsonVo save2(@RequestBody User user) { // 要求前端客户端传一个User类型的JSON字符串过来,请求体是一个JSON
              JsonVo jsonVo = new JsonVo();
              jsonVo.setMsg(service.saveOrUpdate(user) ? "保存成功" : "保存失败");
              return jsonVo;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

    单元测试

    Spring单元测试

    在这里插入图片描述

    在这里插入图片描述

    SpringBoot单元测试

    在这里插入图片描述

            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
    
    
    
    
    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    打包部署

    在这里插入图片描述

    打包部署—jar

    在这里插入图片描述

    打包部署—war

    在这里插入图片描述
    在这里插入图片描述

    注意

    配置JackSON将Model转为JSON时,不包含值为null的属性:

    • application.yml:

      spring:
        jackson:
          default-property-inclusion: non_null
      
      • 1
      • 2
      • 3
    • Java代码:

      public final class JSONs {
          private JSONs() {
          }
      
          private static final ObjectMapper MAPPER = new ObjectMapper();
      
          static {
              MAPPER.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
          }
          public static ObjectMapper getMAPPER() {
              return MAPPER;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    • MySQL配置:

      #url=jdbc:mysql://127.0.0.1:3306/lp_resume
      #url=jdbc:mysql://localhost:3306/lp_resume
      #url=jdbc:mysql:///lp_resume
      
      #UTC:世界同一时间
      #url=jdbc:mysql:///lp_resume?serverTimezone=UTC&useSSL=false
      #中国时间:serverTimezone=Asia/Shanghai == serverTimezone=GMT+8
      #url=jdbc:mysql:///lp_resume?serverTimezone=GMT+8&useSSL=false
      url=jdbc:mysql:///lp_resume?serverTimezone=Asia/Shanghai&useSSL=false
      
      ############使用IDEA连接数据库############
      #使用IDEA连接MySQL数据库时,由于“+”是一个特殊字符,因此需要编码处理为:“%2B”
      #例如:jdbc:mysql:///?serverTimezone=GMT%2B8&useSSL=false
      #或者:jdbc:mysql:///?serverTimezone=Asia/Shanghai&useSSL=false
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    • HTML的button标签,默认类型是,因此button如果是其它类型的话,最好显示声明button的type,比如:

    • 客户端向服务器发送请求参数时

      • 如果http://localhost:8080/jk/dictTypes/list?page=1&size=20,那么服务器获取到的keyword就是null
      • 如果http://localhost:8080/jk/dictTypes/list?page=1&size=20&keyword=,那么服务器获取到的keyword就是""(空字符串)
    • 数据库中,表名和字段名建议使用``、字符串建议使用’'(单引号)

    • MySQL数据库,行(记录)从0开始,列(字段)从1开始

    • 标准JSON格式:key使用""(双引号):

      [
        {
          "age": 10,
          "name": "lp"
        },
        {
          "age": 20,
          "name": "ruoyu"
        }
      ]
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      {
        "string": "value",
        "integer": 10,
        "bool": true,
        "null": null,
        "array": [],
        "obj": {}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

    补充:ChromeJSON插件

    https://chrome.google.com/webstore/detail/json-handle/iahnhfdhidomcpggpaimmmahffihkfnj

    在这里插入图片描述

    参考

    小码哥-李明杰: Java从0到架构师③进阶互联网架构师.


    本文完,感谢您的关注支持!


  • 相关阅读:
    基于docker 配置hadoop-hive-spark-zeppelin环境进行大数据项目的开发
    Timber 架包的使用
    Springboot——如何保证服务启动后不自动停止?
    .Net Core(.Net6)创建grpc
    永磁无刷直流电机(无框力矩电机)电流和速度控制器的设计
    温度及pH敏感性聚乙烯醇/羧甲基壳聚糖水凝胶/金银花多糖/薄荷多糖/O-羧甲基壳聚糖水凝胶
    Python-表白小程序练习
    Ubuntu server 24 (Linux) sudo 免输密码
    Netty入门——概述
    SYNOPSYS VCS Makefile学习
  • 原文地址:https://blog.csdn.net/weixin_44018671/article/details/124588856
  • ${user.name}${user.age}