
核心表 ap_user

建库建表语句
这里一定要使用 navicat,执行SQL 文件,以防止 cmd 中的编码问题
先将 SQL 语句,保存在电脑中,再使用 navicat 打开


- CREATE DATABASE IF NOT EXISTS leadnews_user DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
- USE leadnews_user;
- SET NAMES utf8;
- /*
- Navicat MySQL Data Transfer
- Source Server : localhost
- Source Server Version : 50721
- Source Host : localhost:3306
- Source Database : leadnews_user
- Target Server Type : MYSQL
- Target Server Version : 50721
- File Encoding : 65001
- Date: 2021-04-12 13:58:42
- */
-
- SET FOREIGN_KEY_CHECKS=0;
-
- -- ----------------------------
- -- Table structure for ap_user
- -- ----------------------------
- DROP TABLE IF EXISTS `ap_user`;
- CREATE TABLE `ap_user` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
- `salt` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码、通信等加密盐',
- `name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',
- `password` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码,md5加密',
- `phone` varchar(11) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
- `image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',
- `sex` tinyint(1) unsigned DEFAULT NULL COMMENT '0 男\r\n 1 女\r\n 2 未知',
- `is_certification` tinyint(1) unsigned DEFAULT NULL COMMENT '0 未\r\n 1 是',
- `is_identity_authentication` tinyint(1) DEFAULT NULL COMMENT '是否身份认证',
- `status` tinyint(1) unsigned DEFAULT NULL COMMENT '0正常\r\n 1锁定',
- `flag` tinyint(1) unsigned DEFAULT NULL COMMENT '0 普通用户\r\n 1 自媒体人\r\n 2 大V',
- `created_time` datetime DEFAULT NULL COMMENT '注册时间',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户信息表';
-
- -- ----------------------------
- -- Records of ap_user
- -- ----------------------------
- INSERT INTO `ap_user` VALUES ('1', 'abc', 'zhangsan', 'abc', '13511223453', null, '1', null, null, '1', '1', '2020-03-19 23:22:07');
- INSERT INTO `ap_user` VALUES ('2', 'abc', 'lisi', 'abc', '13511223454', '', '1', null, null, '1', '1', '2020-03-19 23:22:07');
- INSERT INTO `ap_user` VALUES ('3', 'sdsa', 'wangwu', 'wangwu', '13511223455', null, null, null, null, null, '1', null);
- INSERT INTO `ap_user` VALUES ('4', '123abc', 'admin', '81e158e10201b6d7aee6e35eaf744796', '13511223456', null, '1', null, null, '1', '1', '2020-03-30 16:36:32');
- INSERT INTO `ap_user` VALUES ('5', '123', 'suwukong', 'suwukong', '13511223458', null, '1', null, null, '1', '1', '2020-08-01 11:09:57');
- INSERT INTO `ap_user` VALUES ('6', null, null, null, null, null, null, null, null, null, null, null);
-
- -- ----------------------------
- -- Table structure for ap_user_fan
- -- ----------------------------
- DROP TABLE IF EXISTS `ap_user_fan`;
- CREATE TABLE `ap_user_fan` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
- `user_id` int(11) unsigned DEFAULT NULL COMMENT '用户ID',
- `fans_id` int(11) unsigned DEFAULT NULL COMMENT '粉丝ID',
- `fans_name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '粉丝昵称',
- `level` tinyint(1) unsigned DEFAULT NULL COMMENT '粉丝忠实度\r\n 0 正常\r\n 1 潜力股\r\n 2 勇士\r\n 3 铁杆\r\n 4 老铁',
- `created_time` datetime DEFAULT NULL COMMENT '创建时间',
- `is_display` tinyint(1) unsigned DEFAULT NULL COMMENT '是否可见我动态',
- `is_shield_letter` tinyint(1) unsigned DEFAULT NULL COMMENT '是否屏蔽私信',
- `is_shield_comment` tinyint(1) unsigned DEFAULT NULL COMMENT '是否屏蔽评论',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户粉丝信息表';
-
- -- ----------------------------
- -- Records of ap_user_fan
- -- ----------------------------
-
- -- ----------------------------
- -- Table structure for ap_user_follow
- -- ----------------------------
- DROP TABLE IF EXISTS `ap_user_follow`;
- CREATE TABLE `ap_user_follow` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
- `user_id` int(11) unsigned DEFAULT NULL COMMENT '用户ID',
- `follow_id` int(11) unsigned DEFAULT NULL COMMENT '关注作者ID',
- `follow_name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '粉丝昵称',
- `level` tinyint(1) unsigned DEFAULT NULL COMMENT '关注度\r\n 0 偶尔感兴趣\r\n 1 一般\r\n 2 经常\r\n 3 高度',
- `is_notice` tinyint(1) unsigned DEFAULT NULL COMMENT '是否动态通知',
- `created_time` datetime DEFAULT NULL COMMENT '创建时间',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户关注信息表';
-
- -- ----------------------------
- -- Records of ap_user_follow
- -- ----------------------------
-
- -- ----------------------------
- -- Table structure for ap_user_realname
- -- ----------------------------
- DROP TABLE IF EXISTS `ap_user_realname`;
- CREATE TABLE `ap_user_realname` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
- `user_id` int(11) unsigned DEFAULT NULL COMMENT '账号ID',
- `name` varchar(20) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户名称',
- `idno` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '资源名称',
- `font_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '正面照片',
- `back_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '背面照片',
- `hold_image` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手持照片',
- `live_image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '活体照片',
- `status` tinyint(1) unsigned DEFAULT NULL COMMENT '状态\r\n 0 创建中\r\n 1 待审核\r\n 2 审核失败\r\n 9 审核通过',
- `reason` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '拒绝原因',
- `created_time` datetime DEFAULT NULL COMMENT '创建时间',
- `submited_time` datetime DEFAULT NULL COMMENT '提交时间',
- `updated_time` datetime DEFAULT NULL COMMENT '更新时间',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP实名认证信息表';
-
- -- ----------------------------
- -- Records of ap_user_realname
- -- ----------------------------
- INSERT INTO `ap_user_realname` VALUES ('1', '1', 'zhangsan', '512335455602781278', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '9', '', '2019-07-30 14:34:28', '2019-07-30 14:34:30', '2019-07-12 06:48:04');
- INSERT INTO `ap_user_realname` VALUES ('2', '2', 'lisi', '512335455602781279', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '1', '', '2019-07-11 17:21:18', '2019-07-11 17:21:20', '2019-07-12 06:48:04');
- INSERT INTO `ap_user_realname` VALUES ('3', '3', 'wangwu6666', '512335455602781276', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '9', '', '2019-07-11 17:21:18', '2019-07-11 17:21:20', '2019-07-12 06:48:04');
- INSERT INTO `ap_user_realname` VALUES ('5', '5', 'suwukong', '512335455602781279', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbHSAQlqFAAXIZNzAq9E126.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbF6AR16RAAZB2e1EsOg460.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9bbDeAH2qoAAbD_WiUJfk745.jpg', 'http://161.189.111.227/group1/M00/00/00/rBFwgF9ba9qANVEdAAS25KJlEVE291.jpg', '1', '', '2020-08-01 11:10:31', '2020-08-01 11:10:34', '2020-08-01 11:10:36');


dtos 包的创建暂时忽略,不过这里也会讲解 dtos 包的大概作用(存放 DTO 类型)
POJO 的全程有几种,如 pure old java object 、plain ordinary java object 等。但意思都差不多,可以理解为就是简单的 Java 对象,符合没有被继承类、没有实现接口、属性私有、拥有无参构造方法等规则的 Java 对象
POJO 可划分为 VO、DTO、BO、DO 等
| POJO | 全称 | 作用 |
| VO | View Object | 前端会将 VO 内的属性渲染到页面上 |
| DTO | Data Transfer Object | 前后端通过 DTO 传输数据、后端的服务与服务之间也可以通过 DTO 传输数据 |
| BO | Business Object | BO 内会封装多个 DO,即业务直接处理 BO,间接处理 DO。如简历相关业务,简历就是 BO,专业技能、实习经历、项目经历、个人信息等为 DO |
| DO | Data Object | DO 中的属性与数据库表中的属性一一对应,即实体类 |
这里黑马头条中的包划分可能不够严谨,也可能有其他考量,不是重点,暂时不关心
在阿里开发规范中,规定类名中的 DO/BO/DTO/VO/AO/PO/UID 需要大写

- package com.heima.model.user.pojos;
-
- import com.baomidou.mybatisplus.annotation.IdType;
- import com.baomidou.mybatisplus.annotation.TableField;
- import com.baomidou.mybatisplus.annotation.TableId;
- import com.baomidou.mybatisplus.annotation.TableName;
- import lombok.Data;
-
- import java.io.Serializable;
- import java.util.Date;
-
- /**
- *
- * APP用户信息表
- *
- *
- * @author itheima
- */
- @Data
- @TableName("ap_user")
- public class ApUser implements Serializable {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * 主键
- */
- @TableId(value = "id", type = IdType.AUTO)
- private Integer id;
-
- /**
- * 密码、通信等加密盐
- */
- @TableField("salt")
- private String salt;
-
- /**
- * 用户名
- */
- @TableField("name")
- private String name;
-
- /**
- * 密码,md5加密
- */
- @TableField("password")
- private String password;
-
- /**
- * 手机号
- */
- @TableField("phone")
- private String phone;
-
- /**
- * 头像
- */
- @TableField("image")
- private String image;
-
- /**
- * 0 男
- 1 女
- 2 未知
- */
- @TableField("sex")
- private Boolean sex;
-
- /**
- * 0 未
- 1 是
- */
- @TableField("is_certification")
- private Boolean certification;
-
- /**
- * 是否身份认证
- */
- @TableField("is_identity_authentication")
- private Boolean identityAuthentication;
-
- /**
- * 0正常
- 1锁定
- */
- @TableField("status")
- private Boolean status;
-
- /**
- * 0 普通用户
- 1 自媒体人
- 2 大V
- */
- @TableField("flag")
- private Short flag;
-
- /**
- * 注册时间
- */
- @TableField("created_time")
- private Date createdTime;
-
- }
- import com.baomidou.mybatisplus.annotation.IdType;
- import com.baomidou.mybatisplus.annotation.TableField;
- import com.baomidou.mybatisplus.annotation.TableId;
- import com.baomidou.mybatisplus.annotation.TableName;
ApUser 类使用了 MybatisPlus 中的 4 个注解
| 注解 | 作用 |
| @TableName | 让 MybatisPlus 识别当前类对应的表,ApUser > ap_user |
| @TableId | 让 MybatisPlus 识别当前属性为表的主键,id > id |
| @IdType | 让 MybatisPlus 识别当前主键的类型,IdType.AUTO > 自增类型 |
| @TableField | 让 MybatisPlus 识别当前属性对应的表字段,name > name |
添加注解是为了防止数据库字段或 Java 代码属性的命名不规范、不统一
蛇形驼峰转化
如果满足以下三个条件,即可开启蛇形驼峰转化,可以不用上面的 MybatisPlus 注解来进行识别:
在 yml 中开启蛇形驼峰转化:
黑马提供的初始项目的 model 模块下并没有 resources 目录,需要自己创建。然后在 resources 目录下创建 application.yml 文件来进行配置(黑马并没有使用 MybatisPlus 的自动命名转化配置,为了不给后面埋雷,不建议大家做这个配置)

- mybatis-plus:
- configuration:
- map-underscore-to-camel-case: true
MyBatisPlus POM 依赖
在 heima-leadnews-model 模块的 pom.xml 中添加依赖

-
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- dependency>
-
- <dependency>
- <groupId>org.mybatisgroupId>
- <artifactId>mybatisartifactId>
- dependency>
-
- <dependency>
- <groupId>com.baomidougroupId>
- <artifactId>mybatis-plus-boot-starterartifactId>
- dependency>
实际上 mybatis-plus-boot-starter 中包含了 mybatis,但是可能和导入的 mybatis 相比版本什么的有点不同,建议按照黑马的来

import lombok.Data;
| 注解 | 作用 |
| @Data | 为当前类的所有属性添加 getter & setter 方法 |

Lombok POM 依赖
该依赖已经在父工程 pom.xml 中的 dependencies 标签下被引入,在该标签下被引入的依赖会在所有子模块中生效,因此无需再次引入
- <dependency>
- <groupId>org.projectlombokgroupId>
- <artifactId>lombokartifactId>
- <version>${lombok.version}version>
- <scope>providedscope>
- dependency>
微服务搭建口诀:建 module、改 pom、写 yml、主启动、业务类(不绝对,仅提供参考)
黑马提供的初始工程中已经添加好了,不必再次添加
- <dependency>
- <groupId>com.heimagroupId>
- <artifactId>heima-leadnews-modelartifactId>
- dependency>
-
- <dependency>
- <groupId>com.heimagroupId>
- <artifactId>heima-leadnews-commonartifactId>
- dependency>
-
- <dependency>
- <groupId>com.heimagroupId>
- <artifactId>heima-leadnews-feign-apiartifactId>
- dependency>
| 依赖 | 作用 |
| heima-leadnews-model | 导入实体类等,如 ApUser |
| heima-leadnews-common | 导入全局配置、全局功能等,如全局异常捕获 |
| heima-leadnews-feign-api | 导入该模块中统一管理的 feign 远程调用接口(一般微服务之间的调用,都通过 feign 来调用) |
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- dependency>
-
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-testartifactId>
- <scope>testscope>
- dependency>
| 依赖 | 作用 |
| spring-boot-starter-web | 包含 @Controller、@Service、@RequestMapping 等 web 相关注解,实现相关 web 功能 |
| spring-boot-starter-test | 包含 @SpringBootTest、@Test 等测试相关注解,实现相关测试功能 |
SpringBoot 把许多场景抽象为了启动器 starter。当前我们需要一个搭建 web 服务的应用场景,因此使用 starter-web 启动器,即可自动完成 web 服务的所有相关配置(使用默认值),如将使用了 @Bean 注解的类注册进 spring 容器、将内置的 tomcat 配置好等等。与 Spring 相比方便了许多,减少了大量配置文件、配置操作
- <dependency>
- <groupId>com.alibaba.cloudgroupId>
- <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
- dependency>
-
- <dependency>
- <groupId>com.alibaba.cloudgroupId>
- <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
- dependency>
| 依赖 | 作用 |
| spring-cloud-starter-alibaba-nacos-discovery | 实现 nacos 相关服务发现、服务注册等功能 |
| spring-cloud-starter-alibaba-nacos-config | 实现 nacos 相关配置拉取功能 |
注册中心
在微服务架构中,会有某些服务需要由许多机器/容器来提供,而每个机器/容器的 url 都是不一样的。因此我们需要通过服务注册中心将不同的 url 注册到同一个服务名底下,之后再调用该服务名的时候,会从注册中心将服务注册表(包含着 url 与服务名之间映射关系)拉取到本地,然后 nacos 内置的 Ribbon 会将服务名转化为具体的 url(默认轮询,这次是第一个 url,下次就轮到另一个 url),再进行服务调用
配置中心
由于在微服务架构下有许许多多的服务,因此也会产生许许多多的配置文件,每个配置文件也会有许许多多的配置项。当有需求变更时,修改起配置来十分头疼,管理起来也很混乱,很容易改错配置,开发环境直接成为修罗炼狱。而配置中心可以让项目中的配置全部统一管理,并且还可以实现动态修改、运行环境区分(开发、测试、生产等)、配置回滚等功能,很好的解决上述问题
黑马提供初始项目中已经建好了的,不必再重建


由于我们已经在 heima-leadnews-user 的父工程 heima-leadnews-service 中引入了必要的依赖,因此这里暂时不需要添加依赖
- "1.0" encoding="UTF-8"?>
- <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">
- <parent>
- <artifactId>heima-leadnews-serviceartifactId>
- <groupId>com.heimagroupId>
- <version>1.0-SNAPSHOTversion>
- parent>
- <modelVersion>4.0.0modelVersion>
-
- <artifactId>heima-leadnews-userartifactId>
-
- <properties>
- <maven.compiler.source>8maven.compiler.source>
- <maven.compiler.target>8maven.compiler.target>
- properties>
-
- project>

SpringBoot 核心配置文件有两种:bootstrap 和 application
bootstrap
bootstrap 是应用程序的父上下文,由父 ApplicationContext 加载,优先级更高。一般用来做系统层级的参数配置
application
application 是当前应用程序的上下文,优先级较低。一般用来做应用层级的参数配置
为什么用 bootstrap
由于 Spring 根据优先级会先加载 bootstrap 中 nacos 服务的 url(没有就先使用 nacos 默认的),并进行连接尝试,如果你的 nacos 不在默认的 url 上,那么就会报错。因此一定要在 bootstrap 上做好配置,避免报错,才能启动成功
- server:
- port: 51801
- spring:
- application:
- name: leadnews-user
- cloud:
- nacos:
- discovery:
- server-addr: localhost:8848
- config:
- server-addr: localhost:8848
- file-extension: yml
- profiles:
- active: dev
注意使用自己的 nacos 服务地址
nacos 下载
Releases · alibaba/nacos · GitHub
通过 GitHub 下载,可以自行选择版本(我当时下载的是 1.1.4),点击版本下的 Assets 中的 nacos-server-1.1.4.zip(不论 tar.gz/zip 在 linux 和 windows 都能使用,应该)
https://github.com/alibaba/nacos/releases/download/1.1.4/nacos-server-1.1.4.zip

nacos 启动
解压后双击 bin/startup.cmd 启动 nacos(默认端口为 8848)



左边导航栏选择配置列表,点击列表右上方的加号,添加配置

Data ID = spring.application.name (即 leadnews-user) + "-" + spring.profiles.active (即 dev) + "." + spring.cloud.nacos.config.file-extension (即 yml) = leadnews-user-dev.yml

- spring:
- datasource:
- driver-class-name: com.mysql.jdbc.Driver
- url: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
- username: root
- password: 159357
- # 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
- mybatis-plus:
- mapper-locations: classpath*:mapper/*.xml
- # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
- type-aliases-package: com.heima.model.user.pojos
创建 com.heima.user.UserApplication 主启动类(黑马提供的初始工程也建好了)


- package com.heima.user;
-
- import org.mybatis.spring.annotation.MapperScan;
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
-
-
- @SpringBootApplication
- @EnableDiscoveryClient
- @MapperScan("com.heima.user.mapper")
- public class UserApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(UserApplication.class,args);
- }
- }
用来标记当前类为 SpringBoot 的主配置类,SpringBoot 应用由当前类的 main 方法启动,当 mian 方法执行下面这条代码的时候,
SpringApplication.run(UserApplication.class,args);
SpringBoot 将会把当前包底下的 @Bean 方法返回的对象以及 @Component 类的实例化对象自动存放入 Spring 容器中,并且将所有的配置项以默认值自动装配(约定大于配置)
该注解是由 SpringBoot 提供的,拥有服务注册与订阅的相关功能。使用该注解后,SpringBoot 应用会自动向配置的服务注册中心注册自己,并定时发送心跳包告知注册中心自己的服务状态
由 heima-leadnews-model 中的 MyBatis 依赖所提供的注解,可以自动扫描指定包下的类,将扫描到的接口作为 Mapper 并将其实现。有了这个注解可以不必再 XXXMapper 接口上再写 @Mapper 注解了。如果当前服务有许多的 Mapper 接口,那每一个接口上都要写 @Mapper 注解,就会有许多的 @Mapper 注解。如果使用 @MapperScan 注解,即可用一个 @MapperScan 注解代替大量 @Mapper 注解
config、controller、service、mapper

存放配置相关类
为前端提供访问接口,不关心具体业务逻辑,只负责接收参数和返回结果
向 contrller 提供业务逻辑接口,serviceImpl 负责将接口中的方法实现
向 service 提供操作持久层数据的接口,mapper.xml 负责实现接口
这种设计方式体现了一种思想 MVC(即 model + view + controller)
| 分层 | 作用 |
| view | 从 controller 获取数据,并展示给用户 |
| controller | 接收 view 的请求,让 model 进行处理,并返回处理结果给 view |
| model | 操作数据完成业务,将结果返回给 controller |
view 对应项目中的 前端,controller 对应项目中的 controller,model 对应项目中的 service + controller。controller 就如一个中介者,封装 view 和 model 之间的交互,松散耦合,集中控制交互。而 service 与 serviceIpml 分开,mapper 与 mapper.xml 分开,体现了依赖倒转,使调用方不依赖于实现,而是依赖于抽象接口


| 接口路径 | /api/v1/login/login_auth |
| 请求方式 | POST |
| 参数 | LoginDto |
| 响应结果 | ResponseResult |
LoginDto
在 heima-leadnews-model 模块下的 com.heima.model.user.dtos 包下创建 LoginDto

- @Data
- public class LoginDto {
-
- /**
- * 手机号
- */
- private String phone;
-
- /**
- * 密码
- */
- private String password;
- }
ResponseResult
在 heima-leadnews-model 中的 com.heima.model.common.dtos 包底下创建 ResponseResult 类

-
- /**
- * 通用的结果返回类
- * @param
- */
- public class ResponseResult
implements Serializable { -
- private String host;
-
- private Integer code;
-
- private String errorMessage;
-
- private T data;
-
- public ResponseResult() {
- this.code = 200;
- }
-
- public ResponseResult(Integer code, T data) {
- this.code = code;
- this.data = data;
- }
-
- public ResponseResult(Integer code, String msg, T data) {
- this.code = code;
- this.errorMessage = msg;
- this.data = data;
- }
-
- public ResponseResult(Integer code, String msg) {
- this.code = code;
- this.errorMessage = msg;
- }
-
- public static ResponseResult errorResult(int code, String msg) {
- ResponseResult result = new ResponseResult();
- return result.error(code, msg);
- }
-
- public static ResponseResult okResult(int code, String msg) {
- ResponseResult result = new ResponseResult();
- return result.ok(code, null, msg);
- }
-
- public static ResponseResult okResult(Object data) {
- ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getErrorMessage());
- if(data!=null) {
- result.setData(data);
- }
- return result;
- }
-
- public static ResponseResult errorResult(AppHttpCodeEnum enums){
- return setAppHttpCodeEnum(enums,enums.getErrorMessage());
- }
-
- public static ResponseResult errorResult(AppHttpCodeEnum enums, String errorMessage){
- return setAppHttpCodeEnum(enums,errorMessage);
- }
-
- public static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums){
- return okResult(enums.getCode(),enums.getErrorMessage());
- }
-
- private static ResponseResult setAppHttpCodeEnum(AppHttpCodeEnum enums, String errorMessage){
- return okResult(enums.getCode(),errorMessage);
- }
-
- public ResponseResult> error(Integer code, String msg) {
- this.code = code;
- this.errorMessage = msg;
- return this;
- }
-
- public ResponseResult> ok(Integer code, T data) {
- this.code = code;
- this.data = data;
- return this;
- }
-
- public ResponseResult> ok(Integer code, T data, String msg) {
- this.code = code;
- this.data = data;
- this.errorMessage = msg;
- return this;
- }
-
- public ResponseResult> ok(T data) {
- this.data = data;
- return this;
- }
-
- public Integer getCode() {
- return code;
- }
-
- public void setCode(Integer code) {
- this.code = code;
- }
-
- public String getErrorMessage() {
- return errorMessage;
- }
-
- public void setErrorMessage(String errorMessage) {
- this.errorMessage = errorMessage;
- }
-
- public T getData() {
- return data;
- }
-
- public void setData(T data) {
- this.data = data;
- }
-
- public String getHost() {
- return host;
- }
-
- public void setHost(String host) {
- this.host = host;
- }
-
- }
AppHttpCodeEnum
在 heima-leadnews-model 模块中的 com.heima.model.common.enums 包下新建 AppHttpCodeEnum 类

- public enum AppHttpCodeEnum {
-
- // 成功段固定为200
- SUCCESS(200,"操作成功"),
- // 登录段1~50
- NEED_LOGIN(1,"需要登录后操作"),
- LOGIN_PASSWORD_ERROR(2,"密码错误"),
- // TOKEN50~100
- TOKEN_INVALID(50,"无效的TOKEN"),
- TOKEN_EXPIRE(51,"TOKEN已过期"),
- TOKEN_REQUIRE(52,"TOKEN是必须的"),
- // SIGN验签 100~120
- SIGN_INVALID(100,"无效的SIGN"),
- SIG_TIMEOUT(101,"SIGN已过期"),
- // 参数错误 500~1000
- PARAM_REQUIRE(500,"缺少参数"),
- PARAM_INVALID(501,"无效参数"),
- PARAM_IMAGE_FORMAT_ERROR(502,"图片格式有误"),
- SERVER_ERROR(503,"服务器内部错误"),
- // 数据错误 1000~2000
- DATA_EXIST(1000,"数据已经存在"),
- AP_USER_DATA_NOT_EXIST(1001,"ApUser数据不存在"),
- DATA_NOT_EXIST(1002,"数据不存在"),
- // 数据错误 3000~3500
- NO_OPERATOR_AUTH(3000,"无权限操作"),
- NEED_ADMIND(3001,"需要管理员权限");
-
- int code;
- String errorMessage;
-
- AppHttpCodeEnum(int code, String errorMessage){
- this.code = code;
- this.errorMessage = errorMessage;
- }
-
- public int getCode() {
- return code;
- }
-
- public String getErrorMessage() {
- return errorMessage;
- }
- }
- @RestController
- @RequestMapping("/api/v1/login")
- public class ApUserLoginController {
-
- @Autowired
- private ApUserService apUserService;
-
- @PostMapping("/login_auth")
- public ResponseResult login(@RequestBody LoginDto dto){
- return apUserService.login(dto);
- }
- }
@RestController 注解
- @Target({ElementType.TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- @Controller
- @ResponseBody
- public @interface RestController {
- @AliasFor(
- annotation = Controller.class
- )
- String value() default "";
- }
本质是个复合注解,即 @RestController = @Controller + @ResponseBody
| 注解 | 作用 |
| @Target | 规定注解的使用范围( ElementType.TYPE 类注解, ElementType.FIELD 属性注解, ElementType.METHOD 方法注解... ) |
| @Retention | 规定注释的有效时期( RetentionPolicy.SOUTCE 代码注解, RetenionPolicy.CLASS 字节码注解, RetentionPolicy.RUNTIME 运行注解... ) |
| @Documented | 将该注解写入 JAVADOC 中 |
| @Inherited | 使用该注解的类的子类,可以继承该注解 |
| 注解 | 作用 |
| @Controller | 包含 @Component 注解,可以将使用该注解的类的实例化对象注册到 Spring IOC 容器中 |
| @ResponseBody | 将方法的返回值直接写入 HTTP Response 的 body 中 |
@RequestMapping 注解
当请求路径与 @RequestMapping 的 value 值一致时,对请求进行路由,路由到当前类或方法
@AutoWired 注解
自动装载,将注册进容器中的 ApUserService 类的实例化对象取出并注入到当前的 ApUserController 类中。为 ApUserController 提供业务方法(ApUserService 还没建好,可以先不注入)
@RequestBody 与 @RequestParam 的区别
@RequestBody 发送


@RequestBody 接收
- @RestController
- public class TestController {
- @RequestMapping(value = "/test")
- public void test(@RequestBody String body) {
- System.out.println(body);
- }
- }

@RequestParam 发送
第一种 query params


第二种 body form-data


@RequestParam 接收
- @RestController
- public class TestController {
- @RequestMapping(value = "/test")
- public void test(@RequestParam String param) {
- System.out.println(param);
- }
- }
![]()

- public interface ApUserService extends IService
{ - /**
- * app端登录功能
- * @param dto
- * @return
- */
- public ResponseResult login(LoginDto dto);
- }
IService 是由 MyBatis Plus 提供的一个接口,MyBatis 也提供了对应的实现类 ServiceImpl
ServiceImpl 类注入了 baseMapper 接口,而 MyBatis Plus 也实现了该接口
因此我们可以使用 ServiceImpl 中的丰富的基础数据处理方法间接调用 mapper 来进行数据库操作

- @Service
- @Transactional
- @Slf4j
- public class ApUserServiceImpl extends ServiceImpl
implements ApUserService { - /**
- * app端登录功能
- * @param dto
- * @return
- */
- @Override
- public ResponseResult login(LoginDto dto) {
- //1.正常登录 用户名和密码
- if(StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())){
- //1.1 根据手机号查询用户信息
- ApUser dbUser = getOne(Wrappers.
lambdaQuery().eq(ApUser::getPhone, dto.getPhone())); - if(dbUser == null){
- return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
- }
-
- //1.2 比对密码
- String salt = dbUser.getSalt();
- String password = dto.getPassword();
- String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());
- if(!pswd.equals(dbUser.getPassword())){
- return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
- }
-
- //1.3 返回数据 jwt user
- String token = AppJwtUtil.getToken(dbUser.getId().longValue());
- Map
map = new HashMap<>(); - map.put("token",token);
- dbUser.setSalt("");
- dbUser.setPassword("");
- map.put("user",dbUser);
-
- return ResponseResult.okResult(map);
- }else {
- //2.游客登录
- Map
map = new HashMap<>(); - map.put("token",AppJwtUtil.getToken(0L));
- return ResponseResult.okResult(map);
- }
- }
- }
使用 StringUtils 的 isNotBlank 方法判断字符串是否为空、是否有长度、是否含有空白字符
- if(StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())) {
- // 1.正常登录
- } else {
- // 2.游客登录
- Map
map = new HashMap<>(); - map.put("token",AppJwtUtil.getToken(0L));
- return ResponseResult.okResult(map);
- }
游客登陆返回
- {
- "host": "",
- "code": 200, // AppHttpCodeEnum.SUCCESS
- "errorMessage": "操作成功",
- "data": {
- "token": // 使用 AppJwtUtil.getToken(0L) 方法生成游客 token,具体实现下面会讲
- }
- }
使用 ServiceImpl 中的 getOne 方法,用 Wrapper 构造查询条件( dbUser.getPhone() 等与 dto.getPhone)
- //1.1 根据手机号查询用户信息
- ApUser dbUser = getOne(Wrappers.
lambdaQuery().eq(ApUser::getPhone, dto.getPhone())); - if(dbUser == null){
- return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
- }
不存在返回
- {
- "host": "",
- "code": 1002, // AppHttpCodeEnum.DATA_NOT_EXIST
- "errorMessage": "用户信息不存在",
- "data": {}
- }
数据库中的密码加密是通过先加盐再MD5的方式完成的
MD5 为一种单向加密算法,只能加密不能解密。因此很适合用来加密密码,这样连数据库所有者都无法知道用户的真实密码,大大提高安全性。但是既然这么安全为什么还要加盐呢?因为MD5的广泛使用,黑客认为有利可图,于是使用暴力学习,将许多密码进行 MD5 加密,并将映射关系存起来,这样就能用映射表,将 MD5 加密后的密码转为真实的密码。所以,我们通过加盐,也就是给原有的密码加上其他的字符/字符串再进行 MD5 加密,使得暴力解密后的密码也不是真正的密码
使用 org.springframework.util 中的 DigestUtils 类里的 md5DigestAsHex 方法对 用户输入的密码 + 数据库中获取的盐值 进行加密,然后将加密后的输入密码与数据库中的比较
- //1.2 比对密码
- String salt = dbUser.getSalt();
- String password = dto.getPassword();
- String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());
- if(!pswd.equals(dbUser.getPassword())){
- return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
- }
密码错误返回
- {
- "host": "",
- "code": 2, // AppHttpCodeEnum.LOGIN_PASSWORD_ERROR
- "errorMessage": "密码错误",
- "data": {
- "token": // 使用 AppJwtUtil.getToken(0L) 方法生成游客 token,具体实现下面会讲
- }
- }
JWT,即 Json Web Token。它由三部分组成
| header JWT头 | 一个 JSON 对象用来描述 JWT 的签名算法、令牌类型 |
| payload JWT载荷 | 一个 JSON 对象用来传递数据 |
| signature JWT签名 | 使用 base64 加密后的 header + base64 加密后的 payload + 服务器生成的密钥,通过指定算法生成签名 |
形如 headerheaderheader.payloadpayloadpayload.signaturesignaturesignature
即 header 与 payload 与 signature 之间使用 '.' 点号隔开
eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQrDMAzA_uJzA_biuKa_cWnKUigEnMLG2N_nHnaTEPrAMRosUFhUmCmZUEmsImnN-kgZ90q2Z1EWmKDZgIVmRBRS1gn8WuP2t4963t099FnbaWF2bWHWe3B99f9Z-D5bNPz-AKKQJrqAAAAA.0ZNpu7dGgNtoF-UBGM3LvFZxEsltZFHazfByij_IW5KIv82oNxV4VremFE4zAx_lFAxE2XuOZ53LGb1u2mY7Lg
由于 base64 可以解密,因此不要在 payload 中存放重要的隐私信息
为什么需要 JWT
JWT 用来存储客户端与服务器之间的身份识别信息,相当于是存会话信息。既然是会话,为什么不用 session 呢?因为 session 依赖于 cookie,session 中会存放用户信息,浏览器的 cookie 中会存 session 的 id,每次浏览器发送请求的时候就会携带 cookie,这样也能将 cookie 中的 sessionID 发送给服务器,这样服务器就能识别出这个请求时哪个用户发出的。可是有些客户端并没有 cookie,因此也无法存储 sessionID,于是我们就使用 JWT,客户端发送请求的时候带上 JWT,而 JWT 中又有用户的信息,这样服务器也能识别出请求的来源。抛弃掉 session cookie,实现前后端分离
在 heima-leadnews-utils 模块下创建 com.heima.utils.common.AppJwtUtil 类

- public class AppJwtUtil {
-
- // TOKEN的有效期一天(S)
- private static final int TOKEN_TIME_OUT = 3_600;
- // 加密KEY
- private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
- // 最小刷新间隔(S)
- private static final int REFRESH_TIME = 300;
-
- // 生产ID
- public static String getToken(Long id){
- Map
claimMaps = new HashMap<>(); - claimMaps.put("id",id);
- long currentTime = System.currentTimeMillis();
- return Jwts.builder()
- .setId(UUID.randomUUID().toString())
- .setIssuedAt(new Date(currentTime)) //签发时间
- .setSubject("system") //说明
- .setIssuer("heima") //签发者信息
- .setAudience("app") //接收用户
- .compressWith(CompressionCodecs.GZIP) //数据压缩方式
- .signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
- .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
- .addClaims(claimMaps) //cla信息
- .compact();
- }
-
- /**
- * 获取token中的claims信息
- *
- * @param token
- * @return
- */
- private static Jws
getJws(String token) { - return Jwts.parser()
- .setSigningKey(generalKey())
- .parseClaimsJws(token);
- }
-
- /**
- * 获取payload body信息
- *
- * @param token
- * @return
- */
- public static Claims getClaimsBody(String token) {
- try {
- return getJws(token).getBody();
- }catch (ExpiredJwtException e){
- return null;
- }
- }
-
- /**
- * 获取hearder body信息
- *
- * @param token
- * @return
- */
- public static JwsHeader getHeaderBody(String token) {
- return getJws(token).getHeader();
- }
-
- /**
- * 是否过期
- *
- * @param claims
- * @return -1:有效,0:有效,1:过期,2:过期
- */
- public static int verifyToken(Claims claims) {
- if(claims==null){
- return 1;
- }
- try {
- claims.getExpiration()
- .before(new Date());
- // 需要自动刷新TOKEN
- if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
- return -1;
- }else {
- return 0;
- }
- } catch (ExpiredJwtException ex) {
- return 1;
- }catch (Exception e){
- return 2;
- }
- }
-
- /**
- * 由字符串生成加密key
- *
- * @return
- */
- public static SecretKey generalKey() {
- byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
- SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
- return key;
- }
-
- }
生成过程
- //1.3 返回数据 jwt user
- String token = AppJwtUtil.getToken(dbUser.getId().longValue());
- Map
map = new HashMap<>(); - map.put("token",token);
- dbUser.setSalt("");
- dbUser.setPassword("");
- map.put("user",dbUser);
-
- return ResponseResult.okResult(map);
- public static String getToken(Long id){
- Map
claimMaps = new HashMap<>(); - claimMaps.put("id",id);
- long currentTime = System.currentTimeMillis();
- return Jwts.builder()
- .setId(UUID.randomUUID().toString())
- .setIssuedAt(new Date(currentTime)) //签发时间
- .setSubject("system") //说明
- .setIssuer("heima") //签发者信息
- .setAudience("app") //接收用户
- .compressWith(CompressionCodecs.GZIP) //数据压缩方式
- .signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
- .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
- .addClaims(claimMaps) //cla信息
- .compact();
- }
使用 jjwt 依赖提供的 jsonwebtoken 包中的 Jwts.builder 方法构建 jwt,并将用户 id 存放在 jwt 的 payload 中,从而让服务器能够识别请求的发送者

- {
- "host": null,
- "code": 200,
- "errorMessage": "操作成功",
- "data": {
- "user": {
- "id": 4,
- "salt": "",
- "name": "admin",
- "password": "",
- "phone": "13511223456",
- "image": null,
- "sex": true,
- "certification": null,
- "identityAuthentication": null,
- "status": true,
- "flag": 1,
- "createdTime": "2020-03-30T08:36:32.000+00:00"
- },
- "token": "eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWL0QrDIAwA_yXPFZLqau3fpBqZg4IQCxuj_770YW93HPeF12iwAfqEiWRxNefiAu-rYx-z48dMNZKUtSaYoPGAjSIizcHjMoGeu9360SHH3VVNn9IONuOzmHHvxvLu_zOm-2zWwvUD7al0KoAAAAA.5GEoiqO9MH7WCZeBWM95XAlAtlrkHvrbzUqZOO0HktuJjcCCJ20MVprJjXXa-DuNY9qdHIy0yt7Z1ziaflTHUw"
- }
- }
基础用户登录功能完成