LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

P-luminary

Stream模板+Lambda常用+@注释+常用方法模板集合

2024/11/22

Stream模板

中间方法

中间方法的特点惰性求值:中间操作不会立即执行,而是返回一个新的流。实际的计算会在遇到终结方法时进行。可以链式调用:多个中间操作可以链接在一起,形成一个操作链。返回类型:所有的中间操作返回的都是一个 Stream 对象。

Stream中间代码
功能:过滤流中的元素,仅保留满足给定条件的元素。
// 示例:
Stream.of(1, 2, 3, 4, 5)
      .filter(n -> n % 2 == 0) // 只保留偶数
      .forEach(System.out::println);//打印功能:将流中的元素映射为其他形式(通常是不同类型)。
// 示例:
Stream.of("a", "b", "c")
     .map(String::toUpperCase) // 将每个字符串转换为大写功能:将流中的每个元素映射为一个流,并将所有流连接成一个流。示例:Stream<List<String>> listStream = Stream.of(Arrays.asList("a", "b"), Arrays.asList("c", "d"));
listStream
    .flatMap(List::stream) // 将嵌套列表展平为一个流
    .forEach(System.out::println);功能:去除流中的重复元素。示例:Stream.of(1, 2, 2, 3, 4, 4)
     .distinct()
     .forEach(System.out::println); // 输出 1, 2, 3, 4功能:对流中的元素进行排序。示例:Stream.of(5, 3, 1, 4, 2)
     .sorted() // 默认升序排序
     .forEach(System.out::println);功能:截取流中的前 maxSize 个元素。示例:Stream.of(1, 2, 3, 4, 5)
     .limit(3) // 只保留前 3 个元素
     .forEach(System.out::println);功能:跳过流中的前 n 个元素。示例:Stream.of(1, 2, 3, 4, 5)
     .skip(2) // 跳过前 2 个元素
     .forEach(System.out::println); // 输出 3, 4, 5下面是一个示例,展示了多种中间方法的使用:import java.util.Arrays;
import java.util.List;
中间方法:
  1. 中间方法的特点
    惰性求值:中间操作不会立即执行,而是返回一个新的流。实际的计算会在遇到终结方法时进行。
    可以链式调用:多个中间操作可以链接在一起,形成一个操作链。
    返回类型:所有的中间操作返回的都是一个 Stream 对象。
stream中间操作

功能:过滤流中的元素,仅保留满足给定条件的元素。

示例:

Stream.of(1, 2, 3, 4, 5)
.filter(n -> n % 2 == 0) // 只保留偶数
.forEach(System.out::println);//打印

功能:将流中的元素映射为其他形式(通常是不同类型)。

示例:

Stream.of("a", "b", "c")
     .map(String::toUpperCase) // 将每个字符串转换为大写

功能:将流中的每个元素映射为一个流,并将所有流连接成一个流。

示例:

Stream<List<String>> listStream = Stream.of(Arrays.asList("a", "b"), Arrays.asList("c", "d"));
listStream
    .flatMap(List::stream) // 将嵌套列表展平为一个流
    .forEach(System.out::println);

功能:去除流中的重复元素。
示例:

Stream.of(1, 2, 2, 3, 4, 4)
     .distinct()
     .forEach(System.out::println); // 输出 1, 2, 3, 4

功能:对流中的元素进行排序。
示例:

Stream.of(5, 3, 1, 4, 2)
     .sorted() // 默认升序排序
     .forEach(System.out::println);

功能:截取流中的前 maxSize 个元素。
示例:

Stream.of(1, 2, 3, 4, 5)
     .limit(3) // 只保留前 3 个元素
     .forEach(System.out::println);

功能:跳过流中的前 n 个元素。
示例:

Stream.of(1, 2, 3, 4, 5)
     .skip(2) // 跳过前 2 个元素
     .forEach(System.out::println); // 输出 3, 4, 5

下面是一个示例,展示了多种中间方法的使用:

import java.util.Arrays;
import java.util.List;
public class StreamIntermediateOperations {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

    // 使用中间方法
    names.stream()
        .filter(name -&gt; name.startsWith(&quot;A&quot;)) // 过滤以 'A' 开头的名字
        .map(String::toUpperCase) // 将名字转换为大写
        .sorted() // 排序
        .forEach(System.out::println); // 输出结果
}

}


终结方法

在 Java Stream API 中,终结方法(Terminal Operations)是指那些会触发流的计算并最终产生结果的方法。与中间操作不同,终结方法会结束流的操作链,并返回一个具体的结果或副作用。以下是对终结方法的详细介绍:

1. 终结方法的特点

触发计算:终结方法会对流中的数据进行处理并生成结果,通常会遍历流中的所有元素。

返回类型:终结方法可以返回不同类型的结果,包括:

基本类型(如 int、double)

对象(如 List、Set、Map)

特殊值(如 Optional、Void)

终结方法

功能:对流中的每个元素执行指定的操作。

示例:

Stream.of("a", "b", "c").forEach(System.out::println);

功能:将流中的元素收集到集合或其他形式。

示例:

List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());

能:对流中的元素进行归约,返回一个单一的结果。

示例:

int sum = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);

功能:返回流中元素的数量。

示例:

long count = Stream.of("a", "b", "c").count();

功能:检查流中是否有任何元素满足给定的条件。

示例:

boolean hasA = Stream.of("a", "b", "c").anyMatch(s -> s.equals("a"));

功能:检查流中所有元素是否满足给定的条件。

示例:

boolean allMatch = Stream.of(1, 2, 3).allMatch(n -> n < 5);

功能:返回流中的第一个元素(如果存在)。

示例:

Optional<String> first = Stream.of("a", "b", "c").findFirst();

下面是一个示例,展示了多种终结方法的使用:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;


public class StreamTerminalOperations {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

    // forEach
    names.stream().forEach(System.out::println);

    // collect
    List&lt;String&gt; filteredNames = names.stream()
        .filter(name -&gt; name.startsWith(&quot;A&quot;))
        .collect(Collectors.toList());
    System.out.println(filteredNames);

    // reduce
    String concatenated = names.stream()
        .reduce(&quot;&quot;, (a, b) -&gt; a + b);
    System.out.println(concatenated);
    
    // count
    long count = names.stream().count();
    System.out.println(&quot;Count: &quot; + count);
    
    // findFirst
    String firstName = names.stream().findFirst().orElse(&quot;No Name&quot;);
    System.out.println(&quot;First Name: &quot; + firstName);
}

}

stream流超强引用

package com.itheima.pojo.test;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Arrays.asList 是 Java 中 java.util.Arrays 类的一个静态方法,
 * 用于将指定的数组或可变数量的参数转换为一个固定大小的 List。
 * 这个 List 是 ArrayList 的一个内部实现类,
 * 但它不是 java.util.ArrayList,
 * 而是一个不可变的列表
 */
public class Test2 {
    public static void main(String[] args) {
        // List<String> list:将上述列表赋值给 list 变量
        List<String> list = Arrays.asList("apple", "banana", "orange");
        // 定义一个映射,键为整数,值为字符串列表 = 创建一个新的空哈希映射
        // 键的类型是 Integer,值的类型是 List<String> 表示具有相同长度的字符串列表
        // 用HashMap  允许 null 值:键和值都可以为 null,但键只能有一个 null。
        Map<Integer, List<String>> groups = new HashMap<>();
        for (String s : list) {
            int length = s.length();
            // 检查 groups 映射中是否已经存在键为 length 的条目
            if (!groups.containsKey(length)) {
                // 将新创建的列表作为值,以 length 为键添加到 groups 映射中
                // 创建一个新的 ArrayList,并将当前字符串 s 添加到其中
                groups.put(length, new ArrayList<>(Arrays.asList(s)));
            } else {
                // 从 groups 映射中获取键为 length 的列表
                List<String> group = groups.get(length);
                group.add(s);
            }
            System.out.println(groups);
        }

        // 使用 Stream API 进行分组
        // 使用 Collectors.groupingBy 方法按字符串长度进行分组
        // 将分组结果收集到一个新的映射 group2 中。
        Map<Integer, List<String>> group2 = list.stream().collect(Collectors.groupingBy(String::length));
        System.out.println(group2);
    }
}

Lambda+Stream实用方法

// 创建一个包含字符串的列表
List<String> List = Arrays.asList("apple","banana", "orange");
1.for循环输出
for (String s : list){
System.out.println(s);}

2.表达式输出   
list.forEach(s ->
System.out.println(s);});

3.表达式最简洁输出
List.forEach(System.out::println);
// 创建一个包含字符串的列表
List<String> list = Arrays.asList("apple","banana", "orange");

1.使用重写Collections 排序
Collections.sort(list,new Comparator<String>() {
@override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});

2.使用lambda表达式排序
Collections.sort(list,(o1,o2) ->{
    return o1.compareTo(o2)
});

3.使用最简洁的表达式
Collections.sort(list,(o1,o2) ->{o1.compareTo(o2)});
// 创建一个包含字符串的列表
List<String> list = Arrays.asList("apple","banana", "orange");

1.普通方式过滤
List<String> list2 = new ArrayList<>();
List<String s : list2){
if(s.startsWith("a")){
     list2.add(s);
} }

2.使用 Stream API 进行过滤和收集,过滤以 'a' 开头的字符串,收集结果到一个新的 List 中
List<String>list3 = list.stream().filter(s - > s.startsWith("a")).collect(Collectrs.toList());
// 创建一个包含字符串的列表
List<String> list = Arrays.asList("apple","banana", "orange");

1.普通方式获取长度
List<Integer> List2 = new ArrayList<>();
for (String s:list){
    
list2.add(s.length());}

2.Lambda表达式+stream流获取长度
(map)这个函数对我们管道中的每个元素做了处理,在此处为把string转换为Integer类型 主要进行转换作用
List<Integer> list3 = list.stream().map(s -> s.length()).collect(Collectors.toList());
//新建一个List集合
List<Integer> list = Arrays.asList(1,2, 3, 4, 5);
1.普通方式相加操作
int sum =0;
for (Integer v : list) {
SUm +=V  }
System.out.println(sum);

2.Lambda+stream
(表达式含义):【0】操作的起始值,【a = a+b】 循环下去
int sum2 = list.stream().reduce( identity: 0, (a, b) -> a + b);
System.out.println(sum2)
//新建一个集合
List<String> list = Arrays.asList("apple", "banana", "orange");

Map<Integer,List<String>> groups = new HashMap<>();

1.普通方式分组
for (String s:list){
int length = s.length();
if (!groups.containsKey(length)){
groups.put(length,new ArrayList<>());
}
groups.get(Length) .add(s);
}
    System.out.println(groups);

2.Lambda+stream分组
Map<Integer,List<String>> groups2 = list.stream().collect(Collectors.groupingBy(String::length));
System.out.println(groups2)
1.普通方式创建线程

Thread thread = newThread(new Runnable(){
@Override
public void run(){
System.out.println("hello world");
}
});
thread.start();

2.Lambda表达式

Thread thread1 = new Threal(() -> System.out.println(hello world"));
thread1.start();
1.创建接口
interface  MyInterface{
public void doSomething(String s);}

2.普通实现接口
MyInterface myInterface = new MyInterface{
    @override
    public void doSomething(String s){
    System.out.println(s);
    }
};
myInterface.doSomething( s:"hello world");

3.Lambda表达式实现接口
MyInterface myInterface1 = (s) -> System.out.println(s);
myInterface1.doSomething( s:"hello worLd")
String str = "hello world";

1.普通方式
if(str !=null){
System.out.println(str.toUpperCase());}

2.Lambda表达式
Optional.ofNuLlable(str).map(String::toUpperCase).ifPresent(System.out::println);
List<String> List = Arrays.asList("apple","banana", "orange");
1.普通方式
List<String> list2 = new ArrayList<>();
for (String s:list2){ //遍历循环
    if (s.startsWith("a")){ //取出 包含a的元素
        list2.add(s.toUpperCase());//添加到list2中然后转换为大写
    }
}       Collections.sort(list2); //排序

2.Lambda+stream方式

List<String> list3 = list.stream().filter(s -> s.startsWith("a"))
.map(String::toupperCase).sorted().collect(collectors.toList());
public class Dept {
    private int id;

    public Dept(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Dept{id=" + id + "}";
    }
}

public class TestCollectStopOptions {

    public void testCollectStopOptions() {
        // 创建一个包含 Dept 对象的列表
        List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(23));

        // 使用 Stream API 过滤 id 大于 20 的 Dept 对象,并收集到 List 中
        List<Dept> collectList = ids.stream()
                                   .filter(dept -> dept.getId() > 20)
                                   .collect(Collectors.toList());
        System.out.println("collectList: " + collectList);

        // 使用 Stream API 过滤 id 大于 20 的 Dept 对象,并收集到 Set 中
        Set<Dept> collectSet = ids.stream()
                                 .filter(dept -> dept.getId() > 20)
                                 .collect(Collectors.toSet());
        System.out.println("collectSet: " + collectSet);

        // 使用 Stream API 过滤 id 大于 20 的 Dept 对象,并收集到 Map 中,key 为 id,value 为 Dept 对象
        Map<Integer, Dept> collectMap = ids.stream()
                                          .filter(dept -> dept.getId() > 20)
                                          .collect(Collectors.toMap(Dept::getId, dept -> dept));
        System.out.println("collectMap: " + collectMap);
    }

    public static void main(String[] args) {
        new TestCollectStopOptions().testCollectStopOptions();
    }
}

结果

collectList:[Dept{id=22}, Dept{id=23}]
collectSet:[Dept{id=23}, Dept{id=22}]
collectMap:{22=Dept{id=22}, 23=Dept{id=23}}
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class User {
    private String id;

    public User() {
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "User{id='" + id + '\'' + '}';
    }
}

public class TestStringToIntMap {

    /**
     * 演示map的用途:一对一转换
     */
    public void stringToIntMap() {
        // 创建一个包含字符串 ID 的列表
        List<String> ids = Arrays.asList("205", "105", "308", "469", "627", "193", "111");

        // 使用流操作
        List<User> results = ids.stream()
                               .map(id -> {
                                   // 创建一个新的 User 对象
                                   User user = new User();
                                   // 设置 User 对象的 id 属性
                                   user.setId(id);
                                   // 返回 User 对象
                                   return user;
                               })
                               .collect(Collectors.toList()); // 收集结果到一个新的 List 中

        // 打印结果
        System.out.println(results);
    }

    public static void main(String[] args) {
        new TestStringToIntMap().stringToIntMap();
    }
}

@注释笔记

@RequestBody :获取请全体json字符串数据 封装给java对象,封装的前提是 json字符串属性要与实体类属性名一致才可以封装。

—————————————————————————————–

#{name} 占位符 会从方法参数 对象里面调用getname封装方法获取数据映射到占位符位置。

#{参数名} 是Mybatis的参数占位符,可以自动将参数映射到SQL语句去执行

参数名要与接口方法的参数名要一致,但是方法只有一个参数时,参数名可以是任意的。

—————————————————————————————–

Spring MVC 的 @RequestMapping 注解能够处理 HTTP 请求的方法, 比如 GET, PUT, POST, DELETE 以及 PATCH。

所有的请求默认都会是 HTTP GET 类型的。比如@GetMapping

加入路径处理前端响应

—————————————————————————————–

注解@RequiredArgsConstructor 是 Lombok 提供的一个注解,其主要作用在于简化 @Autowired 的书写过程。在编写 Controller 层或 Service 层代码时,常常需要注入众多的 mapper 接口或 service 接口。若每个接口都使用 @Autowired 进行标注,代码会显得繁琐。而 @RequiredArgsConstructor 注解能够替代 @Autowired 注解,但需注意,在类上添加 @RequiredArgsConstructor 时,需要注入的类必须使用 final 进行声明。

—————————————————————————————–

<font style="color:#000000;background-color:rgb(249, 242, 244);">@Repository</font><font style="color:#000000;background-color:rgb(249, 242, 244);">@Repository</font>的作用与<font style="color:#000000;background-color:rgb(249, 242, 244);">@Controller</font><font style="color:#000000;background-color:rgb(249, 242, 244);">@Service</font>的作用都是把对象交给<font style="color:#000000;background-color:rgb(249, 242, 244);">Spring</font>管理。<font style="color:#000000;background-color:rgb(249, 242, 244);">@Repository</font>是标注在<font style="color:#000000;background-color:rgb(249, 242, 244);">Dao</font>层接口上,作用是将接口的一个实现类交给<font style="color:#000000;background-color:rgb(249, 242, 244);">Spring</font>管理。

—————————————————————————————–

@Mapper

@Mapper: 这个注解一般使用在Dao层接口上,相当于一个mapper.xml文件,它的作用就是将接口生成一个动态代理类。加入了@Mapper注解,目的就是为了不再写mapper映射文件。这个注解就是用来映射mapper.xml文件的。

使用@mapper后,不需要在spring配置中设置扫描地址,通过mapper.xml里面的namespace属性对应相关的mapper类,spring将动态的生成Bean后注入到ServiceImpl中

注意:

在Dao层不要存在相同名字的接口,也就是在Dao不要写重载。因为mapper文件是通过id与接口进行对应的,如果写了两个同名的接口,就会导致mapper文件映射出错。

—————————————————————————————–

@Transactional

Spring事务管理-控制事务 注解:@Transactional

作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务 放在类上 则是整个类都会启动事务 放在接口上 接口的实现类都会去启动事务。放在方法上此方法中的代码会启动事务。

—————————————————————————————–

规则:JSON数据的键名与方法形参对象的属性名相同,并需要使用@RequestBody注解标识。

—————————————————————————————–

MultipartFile 接收文件接口

—————————————————————————————–

@Service 表明这个是逻辑层 可以被调用

—————————————————————————————–

@ConfigurationPropertise()

—————————————————————————————–

@Autowired 注入bean

—————————————————————————————–

集合:@RequestParam[List ids

—————————————————————————————–

是一个在Java项目中常用的注解,特别是在使用日志框架如SLF4J时,通过在你的类上使用@Slf4j注解,Lombok会自动为你的类生成一个静态的日志字段,这个字段通常是org.slf4j.Logger类型的,并且通常命名为log。这样,你就可以在类中直接使用log.info(), log.error(), log.debug()等方法来记录日志,而无需手动声明和初始化Logger对象。

—————————————————————————————–

@PathVariable 是 Spring MVC 中用于将 URL 模板变量绑定到你控制器处理器方法参数上的注解。这个注解使得你可以从 URL 中提取出变量值,并将其作为参数传递给控制器的方法。

—————————————————————————————–

@RequestParam(defaultValue = “1”) 给参数设置默认值 如果前端没有参数传进来 默认值为1 可自己设置

—————————————————————————————–

@RequestBody 注解 使用对象去接收 的时候使用的注解

—————————————————————————————–

@RestControllerAdvice

—————————————————————————————–

@ExceptionHandler

—————————————————————————————–

限制请求的方式

@RequestMapping 可以放在类上,获取的路径可以当做所有方法的父路径

@PostMapping(value=”/depts”,method=RequestMethod.GET)注解 可以放方法上,获取前端的路径。

@GetMapping(”/depts”)

@PutMapping

@DeleteMapping

—————————————————————————————–

junnit5

@Test

@ParameterizedTest

@BeforeEach

@AfterEach

@BeforeAll 标识静态方法

@AfterAll 标识静态方法

—————————————————————————————–

@RestController = @Controller+@ResponseBody

标识当前控制类所有方法都有了@ResponseBody

@ResponseBody 将控制器方法直接输出给前端,将java对象转换为json字符串输出给前端

—————————————————————————————–

lombok

—————————————————————————————–

在定义完Filter之后,Filter其实并不会生效,还需要完成Filter的配置,Filter的配置非常简单,只需要在Filter类上添加一个注解:<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">@WebFilter</font>,并指定属性<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">urlPatterns</font>,通过这个属性指定过滤器要拦截哪些请求。

当我们在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。

@Order注解 控制过滤器优先级数字越小 优先级越高

—————————————————————————————–

Spring Boot 中注解的作用

Spring Boot 利用注解来简化配置和提高开发效率。主要注解包括但不限于:

@SpringBootApplication: 启动 Spring Boot 应用程序。

@Component, @Service, @Repository, @Controller: 标记组件,以便 Spring 容器可以自动检测和管理它们。

@Bean 该方法会在spring项目启动时自动调用,并将方法的返回值交给IOC容器管理 – bean对象

@Autowired: 用于自动装配 Bean。

@Bean: 在配置类中定义 Bean。

@Configuration: 定义配置类。

@EnableAutoConfiguration: 开启自动配置。

这些注解通常被组合使用,以提供一个高度可配置且易于扩展的应用程序结构。

12. @PathVariable, @RequestParam, @ModelAttribute, @RequestBody, @ResponseBody

参数绑定

这些注解用于从 HTTP 请求中提取参数,并将它们绑定到方法参数上。

@PathVariable: 用于从 URL 中提取路径变量。

@RequestParam: 用于从查询字符串中提取参数。

@ModelAttribute: 用于将多个请求参数绑定到一个对象上。

@RequestBody: 用于将请求体中的数据绑定到方法参数上。 获取请求体json字符串数据封装给java对象

@ResponseBody: 用于将方法的结果直接写入响应体。

13. @RestController

控制器注解

@RestController 注解是一个组合注解,它等价于 @Controller 和 @ResponseBody 的组合。它表示这是一个 REST 控制器,所有返回值都将被序列化为 JSON 格式并直接写入 HTTP 响应体。

全局异常处理

<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@ControllerAdvice</font> 注解用于定义全局异常处理类,它可以捕获控制器方法抛出的所有异常,并提供统一的错误响应。

@RunWith(SpringRunner.class)

测试运行器

@RunWith(SpringRunner.class) 注解告诉 JUnit 使用 Spring 测试运行器 (SpringRunner) 来运行测试。Spring 测试运行器提供了一种方便的方式来加载 Spring 上下文并管理测试生命周期。

整体测试

<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@SpringBootTest</font> 注解用于执行整体测试,它会加载整个 Spring 应用上下文,包括所有自动配置的 Bean。这对于集成测试非常有用,因为它可以模拟完整的 Spring Boot 应用程序。

@Configuration 用于定义配置类,配置类中的bean可以自动装配到其他bean中

@Configuration类可以使用其他Spring注解,如@ComponentScan和@Import,来扫描组件或导入其他配置类

@Configuration类在Spring容器启动时会通过CGLIB动态代理机制生成代理类,以确保@Bean方法只被调用一次,从而保证单例bean的行为

环境和属性配置:
使用@PropertySource和@Value注解可以将外部属性文件中的值注入到配置类中

—————————————————————————————–

作用:按照一定的条件进行判断,在满足给定条件才会注册对应的bean对象到Spring IOC容器中。

位置:方法、类

@Conditional 本身是一个父注解,派生出大量子注解

@ConditionalonClass:判断环境中是否有对应节码文件才注册bean到IOC容器

对应的bean(类型或名称),才注册bean到IOC容器@ConditionalOnMissingBean:判断环境中没@ConditionalonProperty:判断配置文件中有应属性和值,才注册bean到IOC容器。

—————————————————————————————–

SpringBoot 底层原理

bean获取。

@singleton 默认容器内同名称的bean只有一个实例(单例)

@prototype 每次使用该bean时会创建新的实例(非单例)

@request 每个请求范围内会创建新的实例(web环境中,了解即可)

@session 每个会话范围内都会创建新的实例(web环境中,了解)

@application 每个应用范围内会创建新的实例(web环境中,了解)

@Scope 设置bean的作用域

@Lazy 延迟加载 会延迟到第一次使用的时候才会去加载

默认singleton的bean,在容器启动时被创建,可以使用aLazy注解来延迟初始化(延迟到第一次使用时)

prototype的bean,每一次使用该bean的时候都会创建一个新的实例。

实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性

– 非单例是每次使用时会创建一个全新的bean

@Import 是 Java 中 Spring 框架(特别是 Spring Framework 和 Spring Boot)中用于配置类的一个注解。它主要用于导入其他配置类,使得当前的配置类能够复用其他配置类中的配置信息,从而避免重复的配置代码。

<font style="color:rgb(5, 7, 59);">@Conditional</font> 是 Spring Framework 中的一个注解,它用于在自动配置类(@Configuration 类)中或者通过 <font style="color:rgb(5, 7, 59);">@Bean</font> 方法定义 bean 时,根据特定的条件来决定是否创建某个 bean 或配置。这个注解使得 Spring 的自动配置更加灵活和强大,因为它允许开发者基于特定的条件(如类路径上的特定类、操作系统属性、环境变量等)来启用或禁用配置。

—————————————————————————————–

@Transactional注解书写位置:

  • 方法
    • 当前方法交给spring进行事务管理
    • 当前类中所有的方法都交由spring进行事务管理 (推荐)
  • 接口
    • 接口下所有的实现类当中所有的方法都交给spring 进行事务管理

—————————————————————————————–

  • @Data是lombok注解,可以生成getter/setter方法,tostring/hashcode/equals等方法重写

    @NoArgsConstructor /添加无参构造
    
    @AllArgsConstructor //添加全参构造
    

—————————————————————————————–

@ResponseBody: 将控制器方法返回值直接输出给前端,将java对象转换为json字符串输出给前端@RestController = @controller + @ResponseBody

标识了当前控制器类所有方法就都有了@ResponseBody

@Controller : spring框架的ioc注解,用于给当前类创建实例对象,也就是加入ioc容器中。

@Autowired :依赖注入注解:在运行时会从spring容器中找当前接口实现类对象并注入

@Qualifier(“Bean对象”):指定Bean别名这与对象

@Qualifier常与@Autowired一起使用

@0ptions(useGeneratedKeys = true,keyProperty =”id”)//需要获取数据库赋值的id属性 并赋值给对象的id

—————————————————————————————–

常用方法模板集合

@Test
1:JsONUtil.toJsonStr(paramMap)//将任意对象转换为json字符串形式
                     
//使用hutool工具类把BedDto类型转换成Bed实体类 类型
2:Bed bean = BeanUtil.toBean(bedDto, Bed.class);

//判断对象是否为null 如果为null 返回true
3:Objects.isNull(xx)

//判断对象是否不为null 如果不为null 返回true
4:Objects.nonNull(xx)
                  
//工具类Objects 专门用来解决空指针异常 意思 先判断s1!=null 在调用s1.equals(s2)
5:Objects.equals(s1,s2)

//强转方法
6:String.valueOf() 

//整个对象的转换方法
7:BeanUtils.copyProperties(user,userPojo) 

8:StringUtils.toStringArray(把括号中的内容转换为一个字符串类型数组)

//在java中,JSONOBject类中的get(String key)方法接收一个字符串参数作为键名,用于从JSON对象中获取对应的值,这种方法运行通过建模来检索特定的数据项
//当我们调用JSONOBject.get("acces_token")的时候,实际上是在告诉程序:请查找名为“access_token”的键,并返回其关键的值,
//如果找到匹配的键,则返回相应的值,如果没有找到,则返回null
9:JSONOBject.get("acces_token")
//建造者设计模式:利用各种组件(各种属性)随意组合生成对象,目的是创建对象更加灵活
//与直接调用构造函数对比
//类一般要提供很多构造函数才可以灵活构建对象,这种方式很麻烦
//建造者模式创建对象底层只需需要提供一个构造函数即可,在使用的时候想设置哪个属性就设置哪个属性,最终都调用同一个构造函数
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder给当前类添加建造者设计模式创建对象    这几个注解都要有
10:member = Member.builder()
.openId(openid)
.build();
阅读全文

git详细操作

2024/11/19

@Author yuan

Git作用

Git 作用

代码回溯 版本控制 多人协作 远程备份

Git 简介

Git 是一个分布式版本控制工具,通常用来对软件开发过程中的源代码文件进行管理。通过Git 仓库来存储和管理这些文件,Git 仓库分为两种:

本地仓库:开发人员自己电脑上的 Git 仓库

远程仓库:远程服务器上的 Git 仓库

commit:提交,将本地文件和版本信息保存到本地仓库

push:推送,将本地仓库文件和版本信息上传到远程仓库

pull:拉取,将远程仓库文件和版本信息下载到本地仓库

常用的 Git 代码托管服务

Git中存在两种类型的仓库,即本地仓库和远程仓库。那么我们如何搭建Git远程仓库呢?

我们可以借助互联网上提供的一些代码托管服务来实现,其中比较常用的有GitHub、码云、GitLab等。

获取Git 仓库-从远程仓库克隆

可以通过Git提供的命令从远程仓库进行克隆,将远程仓库克隆到本地命令形式:git clone【远程Git仓库地址】

工作区、暂存区、版本库 概念

版本库:前面看到的.git隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等工作区:包含.git文件夹的目录就是工作区,也称为工作目录,主要用于存放开发的代码暂存区:.git文件夹中有很多文件,其中有一个index文件就是暂存区,也可以叫做stage。暂存区是一个临时保存修改文件的地方

Git工作区中文件的状态

Git工作区中的文件存在两种状态:untracked 未跟踪(未被纳入版本控制)

tracked 已跟踪(被纳入版本控制)

1.Unmodified 未修改状态

2.Modified 已修改状态

3.Staged 已暂存状态

本地仓库操作

本地仓库常用命令如下:

git status 查看文件状态

git add 将文件的修改加入暂存区

git reset 将暂存区的文件取消暂存或者是切换到指定版本

git commit 将暂存区的文件修改提交到版本库

git log 查看日志

Git项目克隆

在IDEA中克隆Git项目

首先配置Git

说明:如果Git安装在默认目录中(C:\Program Files\Git),则IDEA中无需再手动配置,直接就可以使用。

第一步:

第二步:

第三步:

第四步:

第五步:

第六步:

红色:未跟踪文件

绿色:已暂存文件

蓝色:已修改文件

黑色:未修改文件

标签

1.创建一个标签

确认标签名:

添加成功:

推送到远程仓库:

分支操作

分支操作:

查看分支,本质就是执行 gitbranch 命令

创建分支,本质就是执行 git branch 分支名

命令切换分支,本质就是执行 git checkout命令

将分支推送到远程仓库,本质就是执行 git push 命令

合并分支,本质就是执行 git merge 命令

IDEA中查看分支在右下角,如图

新建分支

新建分支推送

切换分支:

合并分支:

合并分支后需要推送到远程仓库同步

切换版本开发

切换版本:

在开发过程中,有a1到a5这几个版本的项目,现在需要重新基于a3去开发后续项目。基于这种情况,我们可以右键这个版本的项目,新建分支进行开发。

新建分支:

提交 && 拉取 && 冲突

1.提交

2.推送

3.拉取

4.提交推送2

5.拉取

操作冲突:多个人操作同一个文件,其中有人基于旧的版本修改,提交新版本会成功,但是推送到远程会失败,就是发送冲突。

冲突为什么发生:a和b同时修改c1文件,a先修改完成c1文件,提交推送到远程仓库,c1文件进行更新版本成为c2,提交推送后b也修改完成c1文件,提交的时候成功 推送则失败,因为远程仓库的文件已经进行了更新。b推送的时候没有拉取最新的文件进行修改,而是使用的过期版本,所以造成冲突。

解决冲突,拉取,合并,推送

操作冲突:

解决冲突操作:1

解决冲突操作:2

解决冲突操作:3

解决冲突操作:4

推送合并后的项目到远程仓库

阅读全文

若依-AI & 帝可得

2024/11/3

RuoYi-Vue

课程版本
JDK 11
MySQL 8
Redis 5
Maven 3.6
Node 16 (Vue3)
  • 技术选型:SpringBoot、SpringSecurity、MyBatis、Jwt、VUE3、Element-Plus
  1. 从VCS导入项目

    • 点击VCS菜单。

    • 选择Get from Version Control...选项。

    • URL:https://gitee.com/y_project/RuoYi-Vue.git
      Directory:C:\Users\Pluminary\Desktop\RuoYi-Vue
      
  2. 输入项目地址

    • 在弹出的窗口中,您可以看到不同的版本控制系统(例如Git, SVN等)。
    • 选择您要导入的项目所使用的版本控制系统。
    • 在接下来的窗口中,您需要输入项目的URL地址。这通常是项目的仓库地址,例如Git仓库的HTTPS或SSH链接。

① 若模块没有导入进去没有亮 就Maven → clean → package

C:\Users\Pluminary\Desktop\RuoYi-Vue\sql 导入Sql文件两个
sql/quartz.sql
sql/ry_20240629.sql

先gitwzs28150/RuoYi-Vue3: :tada: (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统下载这里面的资料,在里面C:\Users\Pluminary\Desktop\itcast>code ./RuoYi-Vue3导入vscode里面,再安装依赖(要进入Vue3里面才能利用package.json去生成)npm install → 运行前端项目npm run dev

// npm install慢的话 就用中国镜像去下载
npm install --registr=https://registry.npmmirror.com

C:\Users\Pluminary\Desktop\itcast\RuoYi-Vue3>npm run dev

> ruoyi@3.8.8 dev
> vite


  VITE v5.3.2  ready in 2026 ms

  ➜  Local:   http://localhost:8080/
  ➜  Network: http://10.254.3.124:8080/
  ➜  Network: http://192.168.104.38:8080/
  ➜  Network: http://192.168.22.1:8080/
  ➜  Network: http://192.168.36.1:8080/
  ➜  press h + enter to show help

再导入课程管理.sql数据库 → 在若依的后台系统 → 系统工具 → 代码生成 → 配置好后下载代码 → 导入数据库

→ 导入RuoYi生成的前端代码C:\Users\Pluminary\Downloads\ruoyi\vue\apicourse导入vscode中 C:\Users\Pluminary\Desktop\itcast\RuoYi-Vue3\src\api\course && C:\Users\Pluminary\Downloads\ruoyi\vue\viewscourse导入vscode中 C:\Users\Pluminary\Desktop\itcast\RuoYi-Vue3\src\api\course

→ 导入RuoYi生成的后端代码C:\Users\Pluminary\Desktop\itcast\RuoYi-Vue3\src\views\coursecom 并且导入配置文件 && C:\Users\Pluminary\Downloads\ruoyi\main\resourcesmapper导入到idea的resource中

功能详解

权限控制
  • 若依内置了强大的权限控制系统,为企业级项目提供了通用的解决方案
    • **demo账号 (超级管理员)**,查看所有功能菜单
    • **zhangsan账号 (市场专员)**,查看线索菜单
    • **yueyue账号 (销售专员)**,查看商机、合同等菜单
  • RBAC (基于角色的控制访问) 是一种广泛使用的访问控制模型,通过角色来分配和管理用户的菜单权限
表名 说明
sys_dept 部门表
sys_post 岗(职)位信息表
sys_menu 菜单权限表
sys_role 角色信息表
sys_role_dept 角色和部门关联表
sys_role_menu 角色和菜单关联表
sys_user 用户信息表
sys_user_post 用户与岗位关联表
sys_user_role 用户和角色关联表

创建新用户小智并关联课研人员角色,仅限课程管理和统计分析菜单访问。

创建菜单
创建角色,并分配权限课研人员
创建用户,并关联角色xiaozhi

若依通过简单的功能配置实现RBC的权限管理
数据字典
  • 若依内置的数据字典,用于维护系统中常见的静态数据。例如:性别、状态
  • 功能包括:字典类型管理、字典数据管理
  • 表关系说明【一对多】
表名 说明
sys_dict_type 字典类型表
sys_dict_data 字典数据表
dict_id dict_name dict_type
1 用户性别 sys_user_sex
2 菜单状态 sys_show_hide
dict_code dict_sort dict_label dict_value dict_type
1 1 0 sys_user_sex
2 2 1 sys_user_sex
3 3 未知 2 sys_user_sex
  • 将一些不经常修改的数据(课程管理的学科字段)改为数据字典维护,以免占用大量空间

    • 添加字典类型和数据

      系统管理 → 字典管理 → 新增 → 添加字典类型
      字典名称:学科
      字典类型:course_subject
      第二页点进去添加字典数据
      javaEE → 0 → 1

    • 修改代码生成信息

      系统工具 → 代码生成 → 编辑课程管理 → subject课程学科 显示类型从文本框改成下拉框 → 字典类型 是学科 → 下载最新的代码
      只需要修改前端vue组件 因为只改了从前端文本框到下拉框 和一些数据字典

    • 下载代码,导入前端

      C:\Users\Pluminary\Downloads\ruoyi (1)\vue\views\course\course\index.vue去替换前端的index.vue 此时去若依前端查看课程管理→课程学科就会发现已经添加新的进去了

此时去课程管理里面找课程学科:JavaEE进行筛选不会出 因为若依底层
http://10.254.2.179/dev-api/course/course/list?pageNum=1&pageSize=10&subject=0
要从数据库里把JavaEE的subject改成0
优点:降低数据库的存储压力 提高磁盘利用率

其他功能
  • 参数设置:对系统中的参数进行动态维护

    系统管理 → 参数设置 → 验证码开关 → 修改 → 参数键值 → false
    还可以开启是否 用户注册功能 → 前端代码隐藏需要修改 → src/views/login.vue → 97行注册开关
    const register = ref(true) 此时登录界面就有立即注册 跳转注册

  • 通知公告(半成品):促进组织内部信息传递

    系统管理 → 通知公告 → 新增

  • 日志管理:轻松追踪用户行为和系统运行状况

    系统管理 → 通知公告 → 日志管理 → 操作日志

系统监控
  • 若依提供了一些列强大的监控工具,能够帮助开发者和运维快速了解应用程序的性能状态

    系统监控 → 在线用户 && 缓存列表
    数据监控【Druid Monitor】 → ruoyi && 123456

定时任务
  • 若依为定时任务功能提供方便友好的web界面,实现动态管理任务

    @Component
    public class MyTask{
      @Scheduled(cron = "0/5 *****?")
      public void showTime(){
         sout("定时任务开始执行:" + new Date());
      }
    }
    // 硬编码 改代码需要重新修改 重新编译 重新上传...
    
  • 每间隔5秒,控制台输出系统时间

    • 创建任务类

      创建一个类 C:\Users\Pluminary\Desktop\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\task\MyTask.java

      package com.ruoyi.quartz.task;
      
      import org.springframework.stereotype.Component;
      
      import java.util.Date;
      
      @Component
      public class MyTask {
          public void showTime(){
              System.out.println("定时任务开始执行:" + new Date());
          }
      }
      
    • 添加任务规则

      系统监控 → 定时任务 → 新增 →
      任务名称:输出时间 任务分组:默认
      调用方式:myTask.showTime()
      Cron表达式生成器:从0秒开始,每5秒执行一次 → 0/5 * * * * ?
      开启输出时间 状态打开!

      然后每间隔5秒就会向控制台输出时间

      定时任务开始执行:Wed Nov 06 18:12:30 CST 2024
      18:12:30.001 [quartzScheduler_Worker-4] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - > Preparing: insert into sys_job_log( job_name, job_group, invoke_target, job_message, status, create_time )values( ?, ?, ?, ?, ?, sysdate() ) 18:12:30.002 [quartzScheduler_Worker-4] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - –> Parameters: 输出时间(String), DEFAULT(String), myTask.showTime()(String), 输出时间 总共耗时:0毫秒(String), 0(String) 18:12:30.009 [quartzScheduler_Worker-4] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - < Updates: 1 定时任务开始执行:Wed Nov 06 18:12:35 CST 2024 18:12:35.006 [quartzScheduler_Worker-5] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - > Preparing: insert into sys_job_log( job_name, job_group, invoke_target, job_message, status, create_time )values( ?, ?, ?, ?, ?, sysdate() ) 18:12:35.006 [quartzScheduler_Worker-5] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - –> Parameters: 输出时间(String), DEFAULT(String), myTask.showTime()(String), 输出时间 总共耗时:0毫秒(String), 0(String) 18:12:35.012 [quartzScheduler_Worker-5] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - < Updates: 1

    • 启动任务

      官方有提供可以训练的模型

      在这里面:com/ruoyi/quartz/task/RyTask.java

表单构建
  • 通过表单构建工具,单独制作一个添加课程的表单页面
    • 制作表单并导出

      系统工具 → 表单构建 → 左侧行容器 → 拖入第一个单行文本 → 右侧可以改名字 如果想实现一行两个文本的化 就把表单栅格调小一点 → 选择性组件的下拉选择托到右面 → 修改字段名、标题、表单栅格…
      日期范围 → 课程有效期 → 选择型组件里的日期范围 命名为:课程有效期
      文件上传组件 → 课程封面
      评分 → 推荐指数
      多行文本 → 课程介绍

    • 复制到前端工程

      搞完后打开前端工程把add.vue它放在 src/views/course/course/中

    • 创建动态菜单

      系统管理 → 菜单管理 → 添加菜单 →
      菜单类型:菜单
      菜单名称:添加课程
      显示排序:1
      路由地址:course/add
      组件路径:course/course/add
      刷新界面 就会有菜单管理→添加课程了

系统工具

代码生成
  • 代码生成器,根据数据库表结构自动生成前后端CRUD代码
  • 提供三种生成模板:单表、树表、主子表(一对多)
  • 树表是一种展示层级数据的表格,能展开折叠,清晰呈现父子关系,便于管理

系统工具 → 代码生成 → 导入部门表 → 编辑 → 生成信息 → 生成模板:树表 →
树编码字段:dept_id:部门id
树父编码字段:parent_id:父部门id
树名称字段:dept_name:部门名称
提交后下载代码
导入后就是 系统管理/部门管理 的树型结构界面了

dept_id parent_id ancestors dept_name
100 0 0 若依科技
101 100 0,100 深圳总公司
102 100 0,100 长沙分公司
108 102 0,100,101 市场部门
109 102 0,100,102 财务部门
系统接口
  • Swagger,能够自动生成API的同步在线文档,并提供Web界面进行接口调用和测试

    系统工具 → 系统接口
    若依的Token在应用程序里 需要搞token进去
    测试:获取用户列表GET
    得到Token(F12后找应用程序 → Cookie若依 → 找到Admin-Token一定是当前ip地址的Token → 在Authorize中设置Token令牌 → 去后端改swagger的请求前缀pathMapping:/因为他的地址前面默认佩戴/dev-api

    # Swagger配置
    swagger:
      # 是否开启swagger
      enabled: true
      # 请求前缀
      pathMapping: /
    

    然后重启后台项目刷新浏览器打开接口再调用Token再去测试
    此时就操作成功了

    {
      "code": 200,
      "msg": "操作成功",
      "data": [
        {
          "userId": 1,
          "username": "admin",
          "password": "admin123",
          "mobile": "15888888888"
        },
        {
          "userId": 2,
          "username": "ry",
          "password": "admin123",
          "mobile": "15666666666"
        }
      ]
    }
    

若依常用功能?

权限控制
数据字典
定时任务
表单构建
代码生成

项目结构

表结构
前端代码分析
  • api/course/course.js 用于向后端发送Ajax请求的接口代码
  • views/course/course/index.vue用于在浏览器展示课程的视图组件
src\views\course\course\index.vue
<template>
  <div class="app-container">
    <!-- :model做双向绑定 将前端录入条件封装给响应对象  v-show控制搜索栏显示隐藏-->
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="课程编码" prop="code">
        <!-- v-model双向绑定code(前端课程编码) clearable清理用户输入信息 @keyup键盘回车事件完成搜索-->
        <el-input
          v-model="queryParams.code"
          placeholder="请输入课程编码"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="课程学科" prop="subject">
        <!-- v-for遍历课程学科的字典数据列表 :lable展示label :value提交value值-->
        <el-select v-model="queryParams.subject" placeholder="请选择课程学科" clearable>
          <el-option
            v-for="dict in course_subject"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="课程名称" prop="name">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入课程名称"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="适用人群" prop="applicablePerson">
        <el-select v-model="queryParams.applicablePerson" placeholder="请选择适用人群" clearable>
          <el-option
            v-for="dict in course_applicable_person"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <!-- @click点击新增按钮弹出新增 v-hasPermi自定义属性完成菜单显示/隐藏 -->
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['course:course:add']"
        >新增</el-button>
      </el-col>
      <!-- :disabled表示这个框是否可用 -->
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['course:course:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['course:course:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="Download"
          @click="handleExport"
          v-hasPermi="['course:course:export']"
        >导出</el-button>
      </el-col>
      <!-- 点击会控制'搜索栏'显示隐藏 @queryTable重新加载表格展示数据 -->
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <!-- v-loading展示表格的加载状态 遍历展示courseList 事件监听器监听选中行 -->
    <el-table v-loading="loading" :data="courseList" @selection-change="handleSelectionChange">
      <!-- 当用户勾选复选框 触发@selection-change -->
      <el-table-column type="selection" width="55" align="center" />
      <!-- 展示具体数据源数据 -->
      <el-table-column label="课程id" align="center" prop="id" />
      <el-table-column label="课程编码" align="center" prop="code" />
      <el-table-column label="课程学科" align="center" prop="subject">
 <!-- 通过scope拿到整个表格数据 通过:value="scope.row.subject"拿到字典值去匹配字典数据列表 找到该值对应的标签显示给前端-->
        <template #default="scope">
          <dict-tag :options="course_subject" :value="scope.row.subject"/>
        </template>
      </el-table-column>
      <el-table-column label="课程名称" align="center" prop="name" />
      <el-table-column label="价格" align="center" prop="price" />
      <el-table-column label="适用人群" align="center" prop="applicablePerson">
        <template #default="scope">
          <dict-tag :options="course_applicable_person" :value="scope.row.applicablePerson"/>
        </template>
      </el-table-column>
      <el-table-column label="课程介绍" align="center" prop="info" />
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<!-- 使用了模板插槽 -->
        <template #default="scope">
          <!-- @click="handleUpdate(scope.row)把当前行的数据传给当前方法  v-hasPermi自定义权限属性-->
          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['course:course:edit']">修改</el-button>
          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['course:course:remove']">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <!-- 分页区域 -->
     <!-- v-show如果大于0条就显示反之隐藏 :total展示总条数 展示分页页码 @pagination="getList"换页后新数据的查询-->
    <pagination
      v-show="total>0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改课程管理对话框 -->
     <!-- el-dialog默认隐藏的 点击会显示 append-to-body默认将对话框在body上追加显示 :title动态绑定标题(新增和修改不一样)-->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <!-- :model来双向绑定 :rules校验规则-->
      <el-form ref="courseRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="课程编码" prop="code">
          <el-input v-model="form.code" placeholder="请输入课程编码" />
        </el-form-item>
        <el-form-item label="课程学科" prop="subject">
          <el-select v-model="form.subject" placeholder="请选择课程学科">
            <el-option
              v-for="dict in course_subject"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="课程名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入课程名称" />
        </el-form-item>
        <el-form-item label="价格" prop="price">
          <el-input v-model="form.price" placeholder="请输入价格" />
        </el-form-item>
        <el-form-item label="适用人群" prop="applicablePerson">
          <el-select v-model="form.applicablePerson" placeholder="请选择适用人群">
            <el-option
              v-for="dict in course_applicable_person"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="课程介绍" prop="info">
          <el-input v-model="form.info" placeholder="请输入课程介绍" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <!-- 提交前先进行表单规则的校验:rules="rules"  -->
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="cancel">取 消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup name="Course">
// 引入后端api接口文件
import { listCourse, getCourse, delCourse, addCourse, updateCourse } from "@/api/course/course";
// 获取当前实例代理对象,用于访问组件数据、方法
const { proxy } = getCurrentInstance();
// 获取课程学科的数据字典
const { course_applicable_person, course_subject } = proxy.useDict('course_applicable_person', 'course_subject');
// 列表数据
const courseList = ref([]);
// 是否显示弹框
const open = ref(false);
// 是否显示加载状态
const loading = ref(true);
// 是否显示搜索栏
const showSearch = ref(true);
// 复选框,被选中id的数组
const ids = ref([]);
// 复选框,是否单选,用于高亮修改、删除按钮
const single = ref(true);
// 复选框,是否多选,仅高亮删除按钮
const multiple = ref(true);
// 总(记录)条数
const total = ref(0);
// 用于区分新增、修改对话框标题
const title = ref("");
// 定义reactive响应式对象
const data = reactive({
  // 新增或修改表单数据
  form: {},
  // 搜索条件参数
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    code: null,
    subject: null,
    name: null,
    applicablePerson: null,
  },
  // 表单校验规则
  rules: {
    code: [
      { required: true, message: "课程编码不能为空", trigger: "blur" }
    ],
    subject: [
      { required: true, message: "课程学科不能为空", trigger: "change" }
    ],
    name: [
      { required: true, message: "课程名称不能为空", trigger: "blur" }
    ],
    price: [
      { required: true, message: "价格不能为空", trigger: "blur" }
    ],
    applicablePerson: [
      { required: true, message: "适用人群不能为空", trigger: "change" }
    ],
    info: [
      { required: true, message: "课程介绍不能为空", trigger: "blur" }
    ],
  }
});
// 将data对象的三个属性,转换为ref响应式对象
const { queryParams, form, rules } = toRefs(data);

/** 查询课程管理列表 */
function getList() {
  loading.value = true;
  listCourse(queryParams.value).then(response => {
    courseList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
}

// 取消按钮
function cancel() {
  open.value = false;
  reset();
}

// 表单重置
function reset() {
  form.value = {
    id: null,
    code: null,
    subject: null,
    name: null,
    price: null,
    applicablePerson: null,
    info: null,
    createTime: null,
    updateTime: null
  };
  proxy.resetForm("courseRef");
}

/** 搜索按钮操作 */
function handleQuery() {
  // 最新的从第一页开始 再发送请求
  queryParams.value.pageNum = 1;
  getList();
}

/** 重置按钮操作 */
function resetQuery() {
  proxy.resetForm("queryRef");
  handleQuery();
}

// 多选框选中数据
// 把选中的复选框对象传递过来
function handleSelectionChange(selection) {
  // 拿到对象调用map方法进行遍历取每个复选框的id
  // 封装给ids的响应式数组对象
  ids.value = selection.map(item => item.id);
  // 控制修改和删除按钮是否高亮可用的 23默认为true
  single.value = selection.length != 1;
  // 修改按钮只要大于0 就是false 那么修改按钮可用使用
  multiple.value = !selection.length;
}

/** 新增按钮操作 */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加课程管理";
}

/** 修改按钮操作 */
// 拿到行对象 重置 取出当前行id或一个id
function handleUpdate(row) {
  reset();
  const _id = row.id || ids.value
  getCourse(_id).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改课程管理";
  });
}

/** 提交按钮 */
function submitForm() {
  // '修改课程'对表单进行校验 正则规则...是否必填
  proxy.$refs["courseRef"].validate(valid => {
    if (valid) {// 区分新增还是修改的操作
      if (form.value.id != null) {
        updateCourse(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addCourse(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}

/** 删除按钮操作 */
function handleDelete(row) {
  // 一行或数组
  const _ids = row.id || ids.value;
  // 防止误操作
  proxy.$modal.confirm('是否确认删除课程管理编号为"' + _ids + '"的数据项?').then(function() {
    return delCourse(_ids);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
}

/** 导出按钮操作 */
function handleExport() {
  proxy.download('course/course/export', {
    ...queryParams.value
  }, `course_${new Date().getTime()}.xlsx`)
}

getList();
</script>
src\api\course\course.js
// 封装了请求和响应拦截器 下面return每个都调用请求
import request from '@/utils/request'

// 查询课程管理列表
// 接收用户输入参数 调用工具类把参数传过去 向后端发送请求完成课程列表的查询
// 然后返回前端并展示数据
export function listCourse(query) {
  return request({
    url: '/course/course/list',
    method: 'get',
    params: query
  })
}

// 查询课程管理详细
// 点击修改按钮时候根据id去查询 返回给前端
export function getCourse(id) {
  return request({
    url: '/course/course/' + id,
    method: 'get'
  })
}

// 新增课程管理
// 当点击确定按钮的时候 就把数据添加进来发送请求后返回前端
export function addCourse(data) {
  return request({
    url: '/course/course',
    method: 'post',
    data: data
  })
}

// 修改课程管理
// 修改完毕(根据id去查找数据库的)
export function updateCourse(data) {
  return request({
    url: '/course/course',
    method: 'put',
    data: data
  })
}

// 删除课程管理
// 批量/单体删除
export function delCourse(id) {
  return request({
    url: '/course/course/' + id,
    method: 'delete'
  })
}

再次熟悉:前+后端结构

若i18n乱码的情况下

file -> settings -> editor -> file ecoding -> default encoding for properties files:utf-8

后端代码分析

  • CourseController

  • ICourseService及实现类

  • CourseMapper及映射方法

  • Course

  • BaseController:web层通用数据处理

/**
     * 查询课程管理列表
     */
    @PreAuthorize("@ss.hasPermi('course:course:list')")
    @GetMapping("/list")
    public TableDataInfo list(Course course) {
        //1.开启分页
        startPage();
        //2.查询课程列表
        List<Course> list = courseService.selectCourseList(course);
        return getDataTable(list);
    }
// 在分页查询那块会附带着两个封装好的sql语句
// select * from tb_course where xxx 【逐步细分成两个小sql】
  1. select count(*) from tb_course where xxx      //总记录数
  2. select * from tb_course where xxx limit ?,?   //获取当前的数据列表

分页原理

AjaxResult:操作消息提醒

权限解读
  • @PreAuthorize注解是Spring Security框架中用来做权限检查的。
  • 它在运行方法前先验证权限,权限够就放行,不够就拦截
@RestController
@RequestMapping("/course/course")
public class CourseController extends BaseController {
    @Autowired
    private ICourseService courseService;

    /**
     * 查询课程管理列表
     */
// 问题:我怎么知道该用户有没有权限呢?基于RBC权限模型 
    @PreAuthorize("@ss.hasPermi('course:course:list')")
    @GetMapping("/list")
    public TableDataInfo list(Course course) {
        startPage();
        List<Course> list = courseService.selectCourseList(course);
        return getDataTable(list);
    }
}
菜单名称 排序 权限标识
课程管理 1 course:course:list
课程管理查询 1 course:course:query
课程管理新增 2 course:course:add
课程管理修改 3 course:course:edit
课程管理删除 4 course:course:remove
课程管理导出 5 course:course:export

PermissionService.java
源码解读在后期

/**
 * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母
 * 
 * @author ruoyi
 */
@Service("ss")
public class PermissionService
{
    /**
     * 验证用户是否具备某权限
     * 
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission)
    {
        if (StringUtils.isEmpty(permission))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        PermissionContextHolder.setContext(permission);
        return hasPermissions(loginUser.getPermissions(), permission);
    }

二次开发 —— 苍穹外卖

若依框架修改器
  • 若依框架修改器是一个可以一键修改RuoYi框架包名、项目名等的工具

E:\Java实例项目1-20套\2024-Java若依框架专题课\01-基础篇\资料\04-二次开发\若依框架修改器.exe

选择系列:RuoYi-Vue
目录名称: sky
项目名:sky
包名:com.sky
artifactId:sky
groupId:com.sky
站点名称:外卖管理系统


C:\Users\Pluminary\Desktop\20241108150415\sky
新建业务模块
  • 新建sky-merchant子模块商家管理

    • 新建子模块
      sky-merchant → Advanced Settings → GroupId:com.sky

    • 父工程版本锁定

      pom.xml(sky总)
      <!-- 商家管理-->
                  <dependency>
                      <groupId>com.sky</groupId>
                      <artifactId>sky-merchant</artifactId>
                      <version>${sky.version}</version>
                  </dependency>
      
      pom.xml(sky-merchant)
      <dependencies>
              <dependency>
                  <groupId>com.sky</groupId>
                  <artifactId>sky-framework</artifactId>
              </dependency>
          </dependencies>
      
    • sky-admin添加依赖

      pom.xml(sky-admin) 
      <dependency>
                  <groupId>com.sky</groupId>
                  <artifactId>sky-merchant</artifactId>
              </dependency>
      

菜品管理

  • 利用若依代码生成器(主子表模板),生成菜品管理的前后端代码
tb_dish【菜品管理】
id
name
price
image
description
status
create_time
update_time
tb_dish_flavor【菜品口味关系表】
id
dish_id
name
value
  • 准备SQL并导入数据库

    E:\Java实例项目1-20套\2024-Java若依框架专题课\01-基础篇\资料\04-二次开发\菜品管理

  • 配置代码生成信息【主子表的生成】

    在若依代码生成 → 导入表tb_dishtb_dish_flavor

  • 下载代码并导入项目

  • 修改代码 → 系统工具 → 代码生成 → 菜品管理(修改) →

    基本信息:实体类名称→Dish 作者→itheima

    字段信息参考页面原型生成 → 系统管理 → 字典管理 →

    字典名称:售卖状态
    字典类型:dish_status
    第二页点进去售卖状态的字典类型 dish_status → 新增

    数据标签:停售 + 起售
    数据键值:0 + 1
    显示排序:1 + 2

​ 回到系统工具 → 代码生成 → 菜品管理修改 → 售卖状态 → 显示类型:下拉框 → 字典类型:售卖状态

→ 代码生成 → 修改菜单配置信息 → 菜品

→ 上面的生成信息

生成模板:主子表
生成模块名:merchant
生成业务名:dish
关联子表的表名:tb_dish_flavor:菜品口味关系表
子表关联的外键名:dish_id:菜品

→ 在代码生成 → tb_dish_flavor菜品口味关系表 → 点击编辑

基本信息:
实体名:DishFalvor

→ 生成代码(main后端 + vue前端 + dishMenu.sql数据库动态菜单) → 动态sql导入进去 → 前端vue将merchant导入到./src/api中,将views导入到views中 → 在前端中将java和resources导入到merchant模块中

对菜品管理进行升级改造

主键隐藏掉 售价前缀¥ 修改时间时分秒
src\views\merchant\dish\index.vue 
把  <el-table-column label="主键" align="center" prop="id" />  打注释符
////////////////////////////////////////////////////////////////////////////
插入¥流程:
<el-table-column label="售价" align="center" prop="price" /> 修改为 →
<el-table-column label="售价" align="center" prop="price">
        <template #default="scope">
          <div>
            ¥{{ scope.row.price }}
          </div>
        </template>
      </el-table-column>
通义灵码操作:
先解析那行代码,然后输入使用vue3语法在售价前显示¥
////////////////////////////////////////////////////////////////////////////
生成年月日时分秒:
<el-table-column label="更新时间" align="center" prop="updateTime" width="180">
        <template #default="scope">
          <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
        </template>
      </el-table-column>
修改为:

修改图片回显bug

新增毛血旺的图片若依默认保存在了本地服务器而不是阿里云上
/profile/upload/2024/11/10/毛血旺_20241110093243A001.jpg

C:\Users\Pluminary\Desktop\20241108150415\sky\sky-admin\src\main\resources\application.yml

# 项目相关配置
ruoyi:
  # 名称
  name: RuoYi
  # 版本
  version: 3.8.8
  # 版权年份
  copyrightYear: 2024
  # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: D:/ruoyi/uploadPath
  # 获取ip地址开关
  addressEnabled: false
  # 验证码类型 math 数字计算 char 字符验证
  captchaType: math

D:\ruoyi\uploadPath\upload\2024\11\10
文件上传组件标签修改 增加&& item.indexOf("http") === -1

watch(() => props.modelValue, val => {
  if (val) {
    // 首先将值转为数组
    const list = Array.isArray(val) ? val : props.modelValue.split(",");
    // 然后将数组转为对象数组
    fileList.value = list.map(item => {
      if (typeof item === "string") {
        if (item.indexOf(baseUrl) === -1 && item.indexOf("http") === -1) {
          item = { name: baseUrl + item, url: baseUrl + item };
        } else {
          item = { name: item, url: item };
        }
      }
      return item;
    });
  } else {
    fileList.value = [];
    return [];
  }
},{ deep: true, immediate: true });
修改口味列表数组格式 改为下拉框的口味搭配
src\views\merchant\dish\index.vue
//------------------------------------------------
// 定义口味名称和口味列表静态数据
const dishFlavorListSelect = ref([
  {name:"辣度", value:["不辣","微辣","中辣","重辣"]},
  {name:"忌口", value:["不要葱","不要蒜","不要香菜","不要辣"]},
  {name:"甜味", value:["无糖","少糖","半糖"]}
]);
//------------------------------------------------
src\views\merchant\dish\index.vue
<template #default="scope">
      <!-- <el-input v-model="scope.row.name" placeholder="请输入口味名称" /> -->
               <!-- label是最终看到下拉框的内容 value是用户提交的内容 -->
           <el-select v-model="scope.row.name"  placeholder="请选择口味名称">
                <el-option
                  v-for="dishFlavor in dishFlavorListSelect"
                  :key="dishFlavor.name"
                  :label="dishFlavor.name" 
                  :value="dishFlavor.name"
                >
                </el-option>
            </el-select>
           </template>
       </el-table-column>
src\views\merchant\dish\index.vue【修改当选中辣度时候 后面的规格】
// 存储当前选中口味列表数组
const checkValueList = ref([]);
// 定义改变口味名称时更新当前选中的口味列表
function changeFlavorName(row){
  // 清空当前行的value
  row.value = [];
  // 根据选中的name去查找静态数据的value
  checkValueList.value = dishFlavorListSelect.value.find(item=>item.name==row.name).value;
}
//增加了一个 @change   注意:如果是多选框一定要 → multiple

<el-table-column label="口味名称" prop="name" width="150">
            <template #default="scope">
              <!-- <el-input v-model="scope.row.name" placeholder="请输入口味名称" /> -->
               <!-- label是最终看到下拉框的内容 value是用户提交的内容 -->
               <el-select v-model="scope.row.name"  placeholder="请选择口味名称"
               @change="changeFlavorName(scope.row)">
                <el-option
                  v-for="dishFlavor in dishFlavorListSelect"
                  :key="dishFlavor.name"
                  :label="dishFlavor.name" 
                  :value="dishFlavor.name"
                >
                </el-option>
               </el-select>
            </template>
          </el-table-column>
          <el-table-column label="口味列表" prop="value" width="150">
            <template #default="scope">
              <!-- <el-input v-model="scope.row.value" placeholder="请输入口味列表" /> -->
          <el-select v-model="scope.row.value"  placeholder="请选择口味列表" multiple>
                <el-option
                  v-for="value in checkValueList"
                  :key="value"
                  :label="value" 
                  :value="value"
                />
          </el-select>
          </template>
          </el-table-column>
//------------------------------------------------
// 定义口味名称和口味列表静态数据
const dishFlavorListSelect = ref([
  {name:"辣度", value:["不辣","微辣","中辣","重辣"]},
  {name:"忌口", value:["不要葱","不要蒜","不要香菜","不要辣"]},
  {name:"甜味", value:["无糖","少糖","半糖"]}
]);


// 存储当前选中口味列表数组
const checkValueList = ref([]);
// 定义改变口味名称时更新当前选中的口味列表
function changeFlavorName(row){
  // 根据选中的name去查找静态数据的value
  checkValueList.value =       dishFlavorListSelect.value.find(item=>item.name==row.name).value;
}
//------------------------------------------------
// 此时报错了 
11:59:00.441 [http-nio-8080-exec-64] ERROR c.s.f.w.e.GlobalExceptionHandler - [handleRuntimeException,100] - 请求地址'/merchant/dish',发生未知异常.
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.lang.String` from Array value (token `JsonToken.START_ARRAY`); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.lang.String` from Array value (token `JsonToken.START_ARRAY`)
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 239] (through reference chain: com.sky.system.domain.Dish["dishFlavorList"]->java.util.ArrayList[0]->com.sky.system.domain.DishFlavor["value"])
    
/*
根据错误日志,问题出在请求地址 /merchant/dish 处理过程中,JSON 解析时出现了类型不匹配的问题。具体来说,Dish 对象中的 dishFlavorList 字段下的 DishFlavor 对象的 value 字段期望接收一个 String 类型的值,但实际接收到的是一个数组。
*/
解决序列化问题bug将口味列表中value通过JSON工具类转换为字符串

提交数据后是字符串 而不是数组

在331行
/** 提交按钮 */
function submitForm() {
  proxy.$refs["dishRef"].validate(valid => {
    if (valid) {
      form.value.dishFlavorList = dishFlavorList.value;
      // 将口味列表中value通过JSON工具类转换为字符串
      form.value.dishFlavorList.forEach(item=>item.value = JSON.stringify(item.value))
      if (form.value.id != null) {
        updateDish(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addDish(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
解决后端添加成功后 修改后口味列表无法回显再增加个非空判断

因为前端提交了字符串給后端,后端再回去修改的时候无法解析字符串 拿到字符串后返回JSON数组即可

/** 修改按钮操作 */
function handleUpdate(row) {
  reset();
  const _id = row.id || ids.value
  getDish(_id).then(response => {
    form.value = response.data;
    dishFlavorList.value = response.data.dishFlavorList;
    // 将口味列表的value字符串转成json数组
    if(dishFlavorList.value!=null){
      form.value.dishFlavorList.forEach(item=>item.value = JSON.parse(item.value))
    }
    open.value = true;
    title.value = "修改菜品管理";
  });
}

/** 提交按钮 */
function submitForm() {
  proxy.$refs["dishRef"].validate(valid => {
    if (valid) {
      form.value.dishFlavorList = dishFlavorList.value;
      // 将口味列表中value通过JSON工具类转换为字符串
      if(form.value.dishFlavorList!=null){
        form.value.dishFlavorList.forEach(item=>item.value = JSON.stringify(item.value))
      }
      if (form.value.id != null) {
        updateDish(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addDish(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}
解决修改时无法修改口味列表的下拉框

給口味下拉框绑定一个获取焦点事件 再该事件内调用方法根据当前行的口味名称去查询静态数据 再赋值給静态数组

// 删除清除策略
// 定义口味列表获取焦点时更新当前选中的口味列表
function FocusFlavorName(row){
  // 根据选中的name去查找静态数据的value
  checkValueList.value = dishFlavorListSelect.value.find(item=>item.name==row.name).value;
}
// 加个 @focus
<el-table-column label="口味列表" prop="value" width="150">
            <template #default="scope">
              <!-- <el-input v-model="scope.row.value" placeholder="请输入口味列表" /> -->
              <el-select v-model="scope.row.value"  placeholder="请选择口味列表" multiple
              @focus = "FocusFlavorName(scope.row)">
                <el-option
                  v-for="value in checkValueList"
                  :key="value"
                  :label="value" 
                  :value="value"
                />
               </el-select>
            </template>
          </el-table-column>

二次开发——页面调整

将原有的页面,调整为外卖管理系统的项目标识
  • 浏览器标签页icon、标题
  • 系统页面中的logo、标题
  • 去除源码 & 文档
  • 主题和自定义图标
  • 登录页面中标题、背景图

icon:public中的favicon.ico 改为 favicon.ico.bak 就作废了
把新的图标复制进来改成favicon.ico

标题:最外层index.html
< title> 外卖管理系统 < /title>
但是改完没有效果 是因为运行环境給覆盖了
.env.development修改一下

logo放在了静态资源页面
src\assets\logo\logo.png

修改顶部源码和文档图标内容
src\layout\components\Navbar.vue

<!-- <el-tooltip content="源码地址" effect="dark" placement="bottom">
          <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
        </el-tooltip>

        <el-tooltip content="文档地址" effect="dark" placement="bottom">
          <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
        </el-tooltip> -->

iconfont-阿里巴巴矢量图标库
下载一个菜品管理的小图标
然后给它复制进去src\assets\icons\svg\菜品管理.svg
随后回到菜单管理修改图标就好

登录界面图片修改:
src\views\login.vue 修改一下标题
背景图 → src\assets\images\login-background1.jpg
通过css样式修改背景图
171行
background-image: url("../assets/images/login-background.jpg");
修改为
background-image: url("../assets/images/login-background1.jpg");

.login {
  display: flex;
  justify-content: right;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/login-background1.jpg");
  background-size: cover;
}
給外卖商家的员工创建角色和用户分配菜单的权限 登录后只能看到自己的功能

系统管理 → 角色管理 → 新增 →
角色名称:商家员工
权限字符串:merchant
角色顺序:4
菜单权限:父子联动
√ 菜品管理

再到用户管理 → 新增 → 添加用户 →
用户昵称:波妞
用户名称:boniu
用户密码:admin123
角色:商家员工

帝可得

  • 帝可得是一个基于物联网概念下的智能售货机运营管理系统

    • 物联网(IOT)

      让各种物品通过互联网链接起来,实现信息的交换和通信

      • 智能家居
      • 共享充电桩
      • 智能售货机
    • 智能售货机

      是物联网技术的一个典型应用

      • 物联网技术
      • 智能分析与推荐
      • 人员设备绑定管理
      • 线上线下融合
  • 售货机术语
    • 区域管理

      为了更高效地进行经营管理,公司将运营范围划分为若干个逻辑区域

    • 点位选择

      点位指的是智能售货机的具体放置位置

    • 售货机功能

      自动小店,摆满了各种零食

    • 货道设计

      售货机里的货道

角色与功能

  • 一个完整的售货机系统由**五端五角色**组成:
  • 管理员:对基础数据(区域、点位、设备、货道、商品等)进行管理
  • 运维人员:投放设备、撤除设备、维修设备。
  • 运营人员:补货。
  • 合作商:仅提供点位,坐收渔翁之利。
  • 消费者:在小程序或屏幕端下单购买商品。

帝可得在线功能文档_Docs

帝可得项目点击链接立即查看 https://codesign.qq.com/s/426304924036117

库表设计

  • 系统后台基础数据表关系说明:

AIGC

  • AI (Artificial Intelligence):即人工智能,是指通过计算机系统模拟人类思维行为一种技术

  • 它通过机器学习、深度学习等算法,使计算机具备对数据分析、理解、推理和决策的能力

  • AIGC (AI Generated content):是AI领域的一个应用分支,专注于利用AI技术自动生成内容

  • 国内常见的通用大模型(AGI)产品:

    • 文心一言
    • 讯飞星火
    • 通义千问
    • KIMI

Prompt的组成

  • 角色:给 AI 定义一个最匹配任务的角色,比如:「你是一位软件工程师」「你是一位小学老师」
  • 指示:对任务进行描述
  • 上下文:给出与任务相关的其它背景信息(尤其在多轮交互中)
  • 例子:必要时给出举例,[实践证明其对输出正确性有帮助]
  • 输入:任务的输入信息;在提示词中明确的标识出输入
  • 输出:输出的格式描述,以便后继模块自动解析模型的输出结果,比如(JSON、Java)

先定义角色,其实就是在开头把问题域收窄,减少二义性。

案例:

角色:你是一位专业的博客作者。

指示:撰写一篇关于最新AI技术发展的文章。

上下文:文章应该涵盖AI技术的当前状态和未来趋势。

例子:可以引用最近的AI技术突破和行业专家的见解。

输入:当前AI技术的相关信息和数据。

输出:一篇结构清晰、观点鲜明的文章草稿。
角色:你是一位资深的Java开发工程师。

指示:编写一个Java函数,该函数接收两个整数参数,并返回它们的和。

上下文:这个函数将被用于一个简单的数学应用程序,该程序帮助学生练习基本的算术运算。

例子:如果你调用函数 `addNumbers(3, 5)`,它应该返回 `8`。

输入:两个整数参数,分别为 `int a` 和 `int b`。

输出:返回这两个整数的和,类型为 `int`。

常见的编程相关的Prompt

表结构

你是一个软件工程师,帮我生成MySQL的表结构
需求如下:
    1,课程管理表,表名tb_course,字段有主键id、课程编码、课程学科、课程名称、课程价格、适用人群、课程介绍
其他要求:
    1,每个表中都有创建时间(create_time)、修改时间(date_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段
    2,每个表的主键都是自增的
    3,课程价格是整型、课程编码是字符串
    4,请为每个字段都添加上comment
    5,帮我给生成的表中插入一些IT课程示例数据
        课程学科:Java、人工智能、大数据
        适用人群:小白学员、中级程序员

生成数据库说明文档

你是一个软件工程师,现在要根据数据库的sql脚本,编写数据库说明文档,sql脚本如下:
CREATE TABLE `tb_course` (
    `id` INT AUTO_INCREMENT COMMENT '主键ID',
    `course_code` VARCHAR(255) NOT NULL COMMENT '课程编码',
    `course_subject` VARCHAR(100) NOT NULL COMMENT '课程学科',
    `course_name` VARCHAR(255) NOT NULL COMMENT '课程名称',
    `course_price` INT COMMENT '课程价格',
    `target_audience` VARCHAR(100) COMMENT '适用人群',
    `course_introduction` TEXT COMMENT '课程介绍',
    `create_time` DATETIME COMMENT '创建时间',
    `update_time` DATETIME COMMENT '修改时间',
    `create_by` VARCHAR(64) COMMENT '创建人',
    `update_by` VARCHAR(64) COMMENT '修改人',
    `remark` VARCHAR(255) COMMENT '备注',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程管理表';

输出要求是:
    1,每个表以及每个表的字段都要详细说明,包括,字段名称、类型、作用
    2,使用markdown的输出格式,字段的描述需要使用表格展示
    3,如果表之间有关系,需要描述清楚表之间的关系

生成代码

代码生成算是比较常规的方案,用的也比较多,分为了几种情况

  • 给出表生成代码(项目中常见)
    • 给出表结构的ddl,可以输出这个表的增删改查的所有代码
    • 给出表结构的dll,可以输出增删改查的接口文档
  • 补全代码
    • 例1-给出实体类,帮助编写getter、setter、toString、构造方法等等
    • 例2-给出一个controller,帮助编写swagger注解等
  • 提取结构(无含金量,费时间的编程)
    • 例1-根据接口文档提取dto类或者vo类

生成代码流程图

有一些比较复杂的业务流程,往往需要画出流程图,现在就可以使用ai协助我们画流程图

你是一个软件工程师,为了方便理解代码执行流程,需要给出代码执行的流程图,代码如下:
    // 创建工单
    @Transactional
    @Override
    public int insertTaskDto(TaskDto taskDto) {
    //1. 查询售货机是否存在
    VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(taskDto.getInnerCode());
    if (vm == null) {
        throw new ServiceException("设备不存在");
    }
    //2. 校验售货机状态与工单类型是否相符
    checkCreateTask(vm.getVmStatus(), taskDto.getProductTypeId());
    //3. 校验这台设备是否有未完成的同类型工单,如果存在则不能创建
    hasTask(taskDto.getInnerCode(), taskDto.getProductTypeId());
    //4. 校验员工是否存在
    Emp emp = empService.selectEmpById(taskDto.getUserId());
    if (emp == null) {
        throw new ServiceException("员工不存在");
    }
    // 5. 校验非同区域下的工作人员不能接受工单
    if (emp.getRegionId() != vm.getRegionId()) {
        throw new ServiceException("非同区域下的工作人员不能接受工单");
    }
    //6. 保存工单信息
    Task task = new Task();
    BeanUtil.copyProperties(taskDto, task);// 属性赋值
    task.setCreateTime(DateUtils.getNowDate());// 创建时间
    task.setTaskCode(generateTaskCode());// 工单编号 202405150001
    task.setTaskStatus(DkdContants.TASK_STATUS_CREATE);// 创建工单
    task.setAddr(vm.getAddr());
    task.setRegionId(vm.getRegionId());
    task.setUserName(emp.getUserName());
    int taskResult = taskMapper.insertTask(task);
    //7. 如果是补货工单,向工单明细表插入记录
    if (task.getProductTypeId() == DkdContants.TASK_TYPE_SUPPLY) {
        if (CollUtil.isEmpty(taskDto.getDetails())) {
            throw new ServiceException("补货工单明细不能为空");
        }
        List<TaskDetails> taskDetailsList = taskDto.getDetails().stream().map(details -> {
            TaskDetails taskDetails = BeanUtil.copyProperties(details, TaskDetails.class);
            taskDetails.setTaskId(task.getTaskId());
            return taskDetails;
        }).collect(Collectors.toList());
        taskDetailsService.insertBatch(taskDetailsList);
    }

    return taskResult;
}

输出:请使用mermaid语言来描述这个方法的流程
graph TD;
    A[查询售货机] --> B(售货机不存在?)
    B -- 是 --> C[抛出异常: 设备不存在]
    B -- 否 --> D[校验售货机状态与工单类型]
    D --> E(校验失败?)
    E -- 是 --> F[抛出异常]
    E -- 否 --> G[检查未完成同类型工单]
    G --> H(存在未完成同类型工单?)
    H -- 是 --> I[抛出异常]
    H -- 否 --> J[查询员工]
    J --> K(员工不存在?)
    K -- 是 --> L[抛出异常: 员工不存在]
    K -- 否 --> M[校验员工区域与售货机区域是否一致]
    M --> N(不一致?)
    N -- 是 --> O[抛出异常: 非同区域工作人员不能接受工单]
    N -- 否 --> P[保存工单信息]
    P --> Q(保存成功?)
    Q -- 否 --> R[处理保存失败]
    Q -- 是 --> S(工单类型为补货?)
    S -- 是 --> T[处理补货工单明细]
    T --> U[补货明细处理结果?]
    U -- 失败 --> V[抛出异常: 补货工单明细不能为空]
    U -- 成功 --> END(结束)
    S -- 否 --> END

项目搭建(后端)

复制gitee克隆的地址https://gitee.com/Pluminary/dkd-parent.git,在新的idea中找到Get from Version Control在URL中导入xxx.git
若左列模块没有高亮 右侧找maven→clean→package
MySQL的配置和导入 → sql里的sql文件导入到Database
C:\Users\Pluminary\Desktop\dkd-parent\dkd-admin\src\main\resources\application-druid.yml
连接好数据库配置信息
Redis的配置(搞个密码)
为了方便学习我没有搞redis的password 项目里的是:root【application.yml的redis里面】
打开redis服务后就可以启动啦dkd-parent\dkd-admin\src\main\java\com\dkd\DkdApplication.java

项目搭建(前端)

通过vscode克隆源码,仓库地址:https://gitee.com/ys-gitee/dkd-vue.git\

  • 打开VS Code,并确保已经安装了Git。如果未安装Git,请先下载并安装。
  • 在VS Code左侧的活动栏中点击”Source Control”图标,或者按下Ctrl+Shift+G,打开Git集成面板。
  • 在Git集成面板上方的输入框中,选择并输入要克隆的Git仓库地址。可以是HTTP或SSH地址。
  • 点击Enter键,VS Code将连接到Git仓库并拉取最新的代码

在vscode中右上角第二个小框框 点击打开命令行 导入jar包 npm install
然后npm run dev打开项目
帝可得管理系统 http://10.254.1.228/index

在VSCode项目中运行npm install命令主要是用于安装项目所需的Node.js包依赖。以下是具体的作用和步骤:

安装依赖包:当你创建一个Node.js项目或者从版本控制系统中克隆一个项目到本地时,项目中通常会包含一个package.json文件。这个文件里列出了项目所有依赖的包及其版本号。运行npm install命令会读取这个文件,然后从npm(Node Package Manager)仓库下载并安装所有列出的依赖包到项目的node_modules目录。

确保环境一致性:通过npm install,可以确保在不同的开发环境和生产环境中,项目使用的是相同版本的依赖包,这有助于避免因为环境差异导致的bug。

项目初始化:如果你是第一次在一个项目中运行npm install,它还会运行每个依赖包的install脚本,这些脚本可能会进行一些设置工作,比如编译代码、生成必要的文件等。

脚本命令:在package.json中,除了依赖项,还可以定义一些脚本命令(scripts)。npm install会使得这些命令变为可用状态,你可以在项目目录下通过npm run <script-name>来执行这些脚本。

[npm 加速,命令行修改国内镜像源【附带国内最新几个镜像】超简约版_npm 修改国内镜像-CSDN博客](https://blog.csdn.net/m0_52172586/article/details/142930356#::text=1.查看目前的镜像源 >npm get registry 2.设置镜像源 >npm,config set registry https%3A %2F%2Fregistry.npmmirror.com 3.验证)

1.查看目前的镜像源
> npm get registry
2.设置镜像源
> npm config set registry https://registry.npmmirror.com
3.验证
> npm get registry
    
/////////////////////////////////////////
后端maven镜像就先设置好maven地址(非C盘)
然后再去配置maven里的文件设置镜像

点位管理

需求说明

业务场景: 假设我们的公司现在有一个宏伟的计划——在北京发展业务。首先,我们需要确定几个有潜力的区域,这些区域可能是人流量大、消费能力高的商业区或居民区。然后,我们要与这些区域内的潜在合作商进行洽谈,比如商场、写字楼、学校等地方的管理者或所有者。

一旦我们与合作商达成协议,确定了合作的细节和点位,我们就可以安排工作人员去投放智能售货机了。这些点位将成为我们智能售货机的“家”,为消费者提供便捷的购买服务。

点位管理主要涉及到三个功能模块,业务流程如下:

  1. 登录系统:后台管理人员登录后台系统
  2. 新增区域: 后台管理人员可以添加区域范围,区域范围与运维/运维人员挂钩,区域下可关联点位。
  3. 新增合作商: 管理人员可以添加合作商,合作商与点位进行关联。
  4. 新增区域点位: 后台管理人员可以在特定区域内新增点位,这些点位是放置智能售货机的具体位置。
graph TD
    A[登录系统] 
    A --> B[新增区域]
    B --> C[新增合作商]
    C --> D[新增区域点位]

库表设计

  • 参考页面原型和具体需求完成库表设计

区域表: 主键id、区域名称、备注说明
合作商表: 主键id、合作商名称、联系人、联系电话、分成比例、账号、密码
点位表: 主键id、点位名称、详细地址、商圈类型、区域外键、合作商外键

// 你是一位软件工程师,帮我生成MySQL的表结构
需求如下:
1,区域表,表名tb_region,字段有主键id、区域名称
2,合作商表,表名tb_partner,字段有主键id、合作商名称、联系人、联系电话、分成比例(int类型)、账号、密码
3,点位表,表名tb_node,字段有主键id、点位名称、详细地址、商圈类型(int类型)
    
其他要求:
1,每张表中都有创建时间(create_time)、修改时间(date_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段
2,每张表的主键都是自增的
3,区域与点位是一对多的关系,合作商与点位是一对多的关系,请用字段表示出来,并建立外键约束
4,请为所有字段都添加上comment
5,帮我给生成的表中插入一些北京城市相关区域、点位、合作商的测试数据
CREATE TABLE `tb_region` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `region_name` VARCHAR(255) NOT NULL COMMENT '区域名称',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='区域表';

-- 插入测试数据
INSERT INTO `tb_region` (`region_name`,`remark`) VALUES ('北京市朝阳区','北京市朝阳区'), ('北京市海淀区','北京市海淀区'), ('北京市东城区','北京市东城区');

CREATE TABLE `tb_partner` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `partner_name` VARCHAR(255) NOT NULL COMMENT '合作商名称',
  `contact_person` VARCHAR(64) COMMENT '联系人',
  `contact_phone` VARCHAR(15) COMMENT '联系电话',
  `profit_ratio` INT COMMENT '分成比例',
  `account` VARCHAR(64) COMMENT '账号',
  `password` VARCHAR(64) COMMENT '密码',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合作商表';

-- 插入测试数据
INSERT INTO `tb_partner` (`partner_name`, `contact_person`, `contact_phone`, `profit_ratio`, `account`, `password`) VALUES
('合作商A', '张三', '13800138000', 30, 'a001', 'pwdA'),
('合作商B', '李四', '13912345678', 25, 'b002', 'pwdB');

CREATE TABLE `tb_node` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `node_name` VARCHAR(255) NOT NULL COMMENT '点位名称',
  `address` VARCHAR(255) NOT NULL COMMENT '详细地址',
  `business_type` INT COMMENT '商圈类型',
  `region_id` INT COMMENT '区域ID',
  `partner_id` INT COMMENT '合作商ID',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注',
  FOREIGN KEY (`region_id`) REFERENCES `tb_region`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (`partner_id`) REFERENCES `tb_partner`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点位表';


-- 插入测试数据
-- 假设区域ID为1对应'北京市朝阳区',合作商ID为1对应'合作商A'
INSERT INTO `tb_node` (`node_name`, `address`, `business_type`, `region_id`, `partner_id`) VALUES
('三里屯点位', '北京市朝阳区三里屯路', 1, 1, 1),
('五道口点位', '北京市海淀区五道口', 2, 2, 2);

对于点位管理数据模型,下面是示意图:

  • 关系字段:region_id、partner_id

  • 数据字典:business_type

生成基础代码

使用若依代码生成器,生成区域管理、合作商管理、点位管理前后端基础代码,导入项目中:
  • 创建目录菜单
  • 添加数据字典
  • 配置代码生成信息
  • 下载代码并导入项目

系统管理 → 菜单管理 → 新增菜单
主类目、点位管理、2、node

→ 系统管理 → 字典管理
字典名称:商圈类型
字典类型:business_type
→ 在第二页找到商圈类型点进去
新增:旅游区
<u>数据标签:旅游区
数据键值:1
显示排序:1
→ 新增:商场写字楼、2、2;学校33、交通枢纽44

→ 系统工具 → 代码生成 → 导入表(tb_node、tb_partner、tb_region)
分别配置表的生成信息
→ 点击区域表字段信息
→ 根据新增区域弹出菜单显示 需要增加区域名称全打勾 备注说明除了查询全打勾 其余全×

→ 点击**合作商表** → 生成信息
包路径:com.dkd.manage、生成模块名:manage、生成业务名:region、生成功能名:区域管理、上级菜单:点位管理
→ 代码生成:Partner
→ 基本信息:
实体类名称:Partner
作者:itheima
→ 字段信息:见**帝可得后台管理系统.md**
→ 生成信息:
生成包路径:com.dkd.manage
生成模块名:manage
生成功能名:合作商管理
上级菜单:点位管理

→ 点击**点位表**
→ 生成信息:
生成包路径:com.dkd.manage
生成模块名:manage
生成功能名:点位管理
上级菜单:点位管理
→ 字段信息:见**帝可得后台管理系统.md**
→ 基本信息:
实体类名称:Node
表描述:点位表
作者:itheima

回到代码生成 选中三张表 生成!!
分别在前后端和数据库 导入java/manage、vue/manage、sql代码

细节:如果当你创建一个模块以后 src.main.java里面没有任何代码 resources里面也没有 它提交仓库的时候是默认空的 所以可以手动添加一个占位符.gitkeep虽然什么都不是,但是可以提交空项目模块

区域管理改造

基础页面

需求

  • 参考页面原型,完成基础布局展示改造
// 让前端页面自动排序
src\views\manage\region\index.vue
<el-table-column label="序号" type="index" width="50" align="center" prop="id" />
区域管理改造
  • 查看详情,需要显示所有区域下所有点位列表(稍后完成)

  • 在查询区域列表时,同时显示每个区域的点位数,还要新增查看详情

    修改后端要参考接口文档,修改前端要参考产品原型

  • 实现此功能方案:

    (1) 同步存储在区域表中有点位数的字段,当点位发生变化时候,同步区域表中的点位数(在tb_region里面新增一个node_count 方案可行考虑缺点:每次点位数据变化时都要更新区域表[增加了工作量],添加数据不一致也会)
    (2) 关联查询编写关联查询语句,在mapper层封装

SQL查询:先聚合统计每个区域的点位数,然后与区域表进行关联查询
[提前在idea中下面的通义灵码状态勾选 本地补全模型云端模型自动触发]
数据库返回的数据 要结合前端所需要的返回数据来写
比如接口文档需要返回remark id name nodeCount ==> select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count

-- 传统模式
-- 1.先聚合统计每个区域下的点位数
-- 确定查询表 tb_node
-- 确定分组字段 region_id
select region_id,count(*) as node_count from tb_node group by region_id;
-- 2.然后与区域表进行关联查询   内连接是两个表的交集
select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count 
   from tb_region r
left join (select region_id,count(*) as node_count 
   from tb_node group by region_id) n 
on r.id=n.region_id;

-- AI辅助编程模式
-- 查询区域表所有的信息,需要显示每个区域的点位数
SELECT r.*, COUNT(n.id) AS node_count FROM tb_region r LEFT JOIN tb_node n ON r.id = n.region_id GROUP BY r.id;
com/dkd/manage/domain/Region.java
package com.dkd.manage.domain;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.dkd.common.annotation.Excel;
import com.dkd.common.core.domain.BaseEntity;

/**
 * 区域管理对象 tb_region
 * 
 * @author itheima
 * @date 2024-11-12
 */
public class Region extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 主键id */
    private Long id;

    /** 区域名称 */
    @Excel(name = "区域名称")
    private String regionName;

    public void setId(Long id) 
    {
        this.id = id;
    }

    public Long getId() 
    {
        return id;
    }
    public void setRegionName(String regionName) 
    {
        this.regionName = regionName;
    }

    public String getRegionName() 
    {
        return regionName;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
            .append("id", getId())
            .append("regionName", getRegionName())
            .append("createTime", getCreateTime())
            .append("updateTime", getUpdateTime())
            .append("createBy", getCreateBy())
            .append("updateBy", getUpdateBy())
            .append("remark", getRemark())
            .toString();
    }
}
com/dkd/manage/domain/vo/RegionVo.java
package com.dkd.manage.domain.vo;

import com.dkd.manage.domain.Region;
import lombok.Data;

@Data
public class RegionVo extends Region {
    // 点位数量
    private Integer nodeCount;

}
com/dkd/manage/mapper/RegionMapper.java
/**
     * 查询区域列表
     * @param regionVo
     * @return
     */
    public List<RegionVo> selectRegionVoList(RegionVo regionVo);
mapper/manage/RegionMapper.xml    
<select id="selectRegionVoList" resultType="com.dkd.manage.domain.vo.RegionVo">
        SELECT r.*, COUNT(n.id) AS node_count FROM tb_region r LEFT JOIN tb_node n ON r.id = n.region_id GROUP BY r.id
    </select>
思考:上面的xml中提取的字段是 node_count 而前端让返回的是驼峰式命名 nodeCount, 若依默认关闭了此功能需要手动开启此功能
dkd-parent → resources → mybatis → mybatis-config.xml
<!-- 使用驼峰命名法转换字段 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
Ctrl+F9是热部署 在不新增文件的时候可以直接部署

RegionMapper

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
public List<RegionVo> selectRegionVoList(Region region);

RegionMapper.xml

<select id="selectRegionVoList" resultType="com.dkd.manage.domain.vo.RegionVo">
select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count from tb_region r
    left join (select region_id,count(*) as node_count from tb_node group by region_id) n on r.id=n.region_id
    <where>
       <if test="regionName != null  and regionName != ''"> and r.region_name like concat('%', #{regionName}, '%')</if>
    </where>
</select> 

IRegionService

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
public List<RegionVo> selectRegionVoList(Region region);

RegionServiceImpl

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
@Override
public List<RegionVo> selectRegionVoList(Region region) {
    return regionMapper.selectRegionVoList(region);
}

RegionController

/**
 * 查询区域管理列表
 */
@PreAuthorize("@ss.hasPermi('manage:region:list')")
@GetMapping("/list")
public TableDataInfo list(Region region)
{
    startPage();
    List<RegionVo> voList = regionService.selectRegionVoList(region);
    return getDataTable(voList);
}

region/index.vue

<!-- 区域列表 -->
<el-table v-loading="loading" :data="regionList" @selection-change="handleSelectionChange">
  <el-table-column type="selection" width="55" align="center" />
  <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
  <el-table-column label="区域名称" align="center" prop="regionName" />
  <el-table-column label="点位数" align="center" prop="nodeCount" />
  <el-table-column label="备注说明" align="center" prop="remark" />
  <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
    <template #default="scope">
      <el-button link type="primary"  @click="handleUpdate(scope.row)" v-hasPermi="['manage:region:edit']">修改</el-button>
      <el-button link type="primary"  @click="handleDelete(scope.row)" v-hasPermi="['manage:region:remove']">删除</el-button>
    </template>
  </el-table-column>
</el-table>
小结
  • 区域列表改造步骤
    • 确定关联查询方案并编写sql
    • 创建RegionVo
    • 在RegionMapper和xml中添加查询Vo方法和sql
    • 在RegionService接口和实现类中添加查询Vo方法
    • 修改RegionController查询方法
    • 修改前端视图组件

合作商改造-查看详情

密码是明文 改成密文
<el-form-item label="密码" prop="password">
   <el-input v-model="form.password" type="password" placeholder="请输入密码" />
</el-form-item>
隐藏修改时的账号密码id存在时隐藏不存在显示,因为修改和新增共用了一个对话框ui
<el-form-item label="账号" prop="account" v-if="form.id==null">
    <el-input v-model="form.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="form.id==null">
    <el-input v-model="form.password" type="password" placeholder="请输入密码" />
</el-form-item>
前端需要返回创建时间因为数据返回时有,用v-if判断是否修改显示创建时间
 <el-form-item label="创建时间" prop="contactPhone" v-if="form.id!=null">
          {{form.createTime}}
        </el-form-item>
新增时保存的数据是以明文保存到了数据库此时新增的合作商就是密文了
com/dkd/manage/service/impl/PartnerServiceImpl.java  
/**
     * 新增合作商管理
     *
     * @param partner 合作商管理
     * @return 结果
     */
    @Override
    public int insertPartner(Partner partner) {
        // 使用SpringSecurity工具类,对前端传入的密码进行加密
        partner.setPassword(SecurityUtils.encryptPassword(partner.getPassword()));
        partner.setCreateTime(DateUtils.getNowDate());
        return partnerMapper.insertPartner(partner);
    }
合作商管理改造—合作商详情
  • 查看详情,需要显示合作商名称、联系人、联系电话、分成比例
  • 在查询合作商列表时,同时显示每个合作商的点位数
  • 重置密码,初始密码为123456
/** 借鉴修改流程
/** 修改按钮操作 */
function handleUpdate(row) {
  reset();
  const _id = row.id || ids.value
  getPartner(_id).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改合作商管理";
  });
}
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  <template #default="scope">
    <el-button link type="primary" icon="Edit" @click="getPartnerInfo(scope.row)" v-hasPermi="['manage:partner:query']">查看详情</el-button>
  </template>
</el-table-column>
com/dkd/manage/controller/PartnerController.java
    /**
     * 获取合作商管理详细信息
     */
    @PreAuthorize("@ss.hasPermi('manage:partner:query')")
    @GetMapping(value = "/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id)
    {
        return success(partnerService.selectPartnerById(id));
    }
/** 查看合作商详情 **/
  const partnerInfoOpen = ref(false)
  function getPartnerInfo(row){
    reset();
    const _id = row.id
    getPartner(_id).then(response => {
      form.value = response.data;
      partnerInfoOpen.value = true;
    });
  }
    <!-- 查看合作商详情对话框 -->
<el-dialog title="合作商详情" v-model="partnerInfoOpen" width="500px" append-to-body>
    <!-- 使用el-descriptions组件以卡片形式展示信息,更加整洁 -->
    <el-descriptions :column="2" border>
        <el-descriptions-item label="合作商名称">{{ form.partnerName }}</el-descriptions-item>
        <el-descriptions-item label="联系人">{{ form.contactPerson }}</el-descriptions-item>
        <el-descriptions-item label="联系电话">{{ form.contactPhone }}</el-descriptions-item>
        <el-descriptions-item label="分成比例">{{ form.profitRatio }}%</el-descriptions-item>
    </el-descriptions>
</el-dialog>

合作商改造—列表查询点位管理→合作商管理→增加点位数

  • 实现此功能方案:

后端改造

关联查询编写关联查询语句,在mapper层封装

tb_node(点位表)的partner_id(合作商ID)
关联
tb_partner(合作商表)的id

-- 查询合作商表的所有信息,同时显示每个合作商的点位数
select p.*, count(n.id) as node_count
from tb_partner p
    left join tb_node n on p.id = n.partner_id
group by p.id

首先先创建一个需要查询新东西的方法增加

com/dkd/manage/domain/vo/PartnerVo.java
package com.dkd.manage.domain.vo;

import com.dkd.manage.domain.Partner;
import lombok.Data;

@Data
public class PartnerVo extends Partner {
    // 点位数量
    private Integer nodeCount;
}
com/dkd/manage/controller/PartnerController.java
 /**
     * 查询合作商管理列表
     */
    @PreAuthorize("@ss.hasPermi('manage:partner:list')")
    @GetMapping("/list")
    public TableDataInfo list(Partner partner) {
        startPage();
        List<PartnerVo> voList = partnerService.selectPartnerVoList(partner);
        return getDataTable(voList);
    }
com/dkd/manage/service/IPartnerService.java
/**
     * 查询合作商列表
     * @param partner
     * @return
     */
    public List<PartnerVo> selectPartnerVoList(Partner partner);
com/dkd/manage/service/impl/PartnerServiceImpl.java
/**
     * 查询合作商列表
     * @param partner
     * @return
     */
    @Override
    public List<PartnerVo> selectPartnerVoList(Partner partner) {
        return partnerMapper.selectPartnerVoList(partner);
    }
com/dkd/manage/mapper/PartnerMapper.java
/**
     * 查询合作商列表
     * @param partner
     * @return
     */
    public List<PartnerVo> selectPartnerVoList(Partner partner);
mapper/manage/PartnerMapper.xml
</select>
        <select id="selectPartnerVoList" resultType="com.dkd.manage.domain.vo.PartnerVo">
        select p.*, count(n.id) as node_count
        from tb_partner p
                 left join tb_node n on p.id = n.partner_id
        <where>
            <if test="partnerName != null  and partnerName != ''"> and partner_name like concat('%', #{partnerName}, '%')</if>
        </where>
        group by p.id
    </select>

前端改造

<el-table-column type="selection" width="55" align="center" />
      <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
      <el-table-column label="合作商名称" align="center" prop="partnerName" />
      <el-table-column label="点位数" align="center" prop="nodeCount" />
      <el-table-column label="账号" align="center" prop="account" />
      <el-table-column label="分成比例" align="center" prop="profitRatio">
        <template #default="scope">
          {{ scope.row.profitRatio + '%' }}
        </template>
      </el-table-column>
      <el-table-column label="联系人" align="center" prop="contactPerson" />
      <el-table-column label="联系电话" align="center" prop="contactPhone" />
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">

合作商改造——重置密码

  • 查看详情,需要显示合作商名称、联系人、联系电话、分成比例
  • 在查询合作商列表时,同时显示每个合作商的点位数
  • 重置密码,初始密码为123456

后端部分

在PartnerController中

/**
 * 重置合作商密码
 */
@PreAuthorize("@ss.hasPermi('manage:partner:edit')")
@Log(title = "重置合作商密码", businessType = BusinessType.UPDATE)
@PutMapping("/resetPwd/{id}")
public AjaxResult resetpwd(@PathVariable Long id) {//1. 接收参数
    //2. 创建合作商对象
    Partner partner = new Partner();
    partner.setId(id);// 设置id
    partner.setPassword(SecurityUtils.encryptPassword("123456"));// 设置加密后的初始密码
    //3. 调用service更新密码
    return toAjax(partnerService.updatePartner(partner));
}

前端部分

manage/partner.js请求api中

// 重置合作商密码
export function resetPartnerPwd(id){
  return request({
    url: '/manage/partner/resetPwd/' + id,
    method: 'put'
  })
}

partner/index.vue视图组件中参考@click=”handleDelete”,此方法删除时弹出对话框

<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="300px">
    <template #default="scope">
        <el-button link type="primary" @click="resetPwd(scope.row)" v-hasPermi="['manage:partner:edit']">重置密码</el-button>
    </template>
</el-table-column>

<script>
    import { listPartner, getPartner, delPartner, addPartner, updatePartner,resetPartnerPwd } from "@/api/manage/partner";
    /* 重置合作商密码 */
    function resetPwd(row) {
        proxy.$modal.confirm('你确定要重置该合作商密码吗?').then(function () {
            return resetPartnerPwd(row.id);
        }).then(() => {
            proxy.$modal.msgSuccess("重置成功");
        }).catch(() => { });
    }
</script>

点位管理改造——基础布局

修改前端更简单一些 可以把前端一次性请求1w条数据 后端接口就可以重复调用

src\views\manage\node\index.vue视图组件中

import {listRegion} from "@/api/manage/region";

/* 查询所有条件对象 */
const loadAllParams=reactive({
  pageNum:1,
  pageSize:10000
})
/** 查询区域列表 **/
const regionList=ref([]);
function getRegionList() {
  listRegion(loadAllParams).then(response=>{
    regionList.value=response.rows;
  })
}
getRegionList();
getList();

src\api\manage\region.js

// 查询区域管理列表
export function listRegion(query) {
  return request({
    url: '/manage/region/list',
    method: 'get',
    params: query
  })
}
新增点位管理的时候想把合作商输入的改成自动获取的下拉框
  • 定义js代码向后台发送请求,将请求后的结果封装给合作商parnterList集合
  • 将文本框改成下拉框来遍历展示每个合作商的名称,提交时关联合作商的id
      <el-table-column label="详细地址" align="center" prop="address" show-overflow-tooltip/> 详细地址多出的部分隐藏只有鼠标移动到才会显示


import {listPartner} from "@/api/manage/partner"

<el-form-item label="合作商ID" prop="partnerId">
          <!-- <el-input v-model="form.partnerId" placeholder="请输入合作商ID" /> -->
           <el-select v-model="form.partnerId" placeholder="请选择合作商">
            <el-option
              v-for="item in partnerList"
              :key="item.id"
              :label="item.partnerName"
              :value="item.id">
            </el-option>
           </el-select>
</el-form-item>

/* 查询合作商列表 */
const partnerList=ref([]);
function getPartnerList(){
  listPartner(loadAllParams).then(response=>{
    partnerList.value = response.rows;
  })
}
getPartnerList();
避免每次都要写pageSize:10000 直接搞入js里面
src\api\page.js
/* 查询所有条件对象 */
// const loadAllParams=reactive({
//   pageNum:1,
//   pageSize:10000
// }) →

export const loadAllParams = reactive({
  pageNum: 1,
  pageSize: 10000,
});

点位管理改造点位中增加个查看详情(将单表查询改为多表查询咯)

  • 查看详情,需要显示当前点位下所有设备列表(稍后完成)
  • 在区域详情中,需要显示每个点位的设备数
  • 在点位列表查询中,关联显示区域、合作商等信息
  • 关联查询:对于设备数量的统计,我们需要执行关联查询,在mapper层封装
  • 关联实体对于区域和合作商的数据,我们会采用Mybatis提供的嵌套查询功能

点位管理改造表设计.png

<resultMap>......</resultMap>   #完成手动映射

#解决一对一 或 多对一 映射结果集只有一个对象时完成的ORM的映射封装
<association>......</association>  #点位和点位1对1  点位和合作商1对多

#解决一对多场景下来映射多个结果的集合 单个区域表+区域点位列表 映射的是集合!!
<collection>......</collection> 
# AI辅助编程模式
-- AI辅助编程模式
-- 你是一个软件开发工程师,现在要根据数据库的sql脚本,查询并显示点位表所有的字段信息,同时显示每个点位的设备数量,sql脚本如下:
create table tb_node
(
    id            int auto_increment comment '主键id'
        primary key,
    node_name     varchar(255)                        not null comment '点位名称',
    address       varchar(255)                        not null comment '详细地址',
    business_type int                                 null comment '商圈类型',
    region_id     int                                 null comment '区域ID',
    partner_id    int                                 null comment '合作商ID',
    create_time   timestamp default CURRENT_TIMESTAMP null comment '创建时间',
    update_time   timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间',
    create_by     varchar(64)                         null comment '创建人',
    update_by     varchar(64)                         null comment '修改人',
    remark        text                                null comment '备注',
    constraint tb_node_ibfk_1
        foreign key (region_id) references tb_region (id)
            on update cascade on delete cascade,
    constraint tb_node_ibfk_2
        foreign key (partner_id) references tb_partner (id)
            on update cascade on delete cascade
)
    comment '点位表';
    
create table tb_vending_machine
(
    id                   bigint auto_increment comment '主键'
        primary key,
    inner_code           varchar(15) default '000'                 null comment '设备编号',
    channel_max_capacity int                                       null comment '设备容量',
    node_id              int                                       not null comment '点位Id',
    addr                 varchar(100)                              null comment '详细地址',
    last_supply_time     datetime    default '2000-01-01 00:00:00' not null comment '上次补货时间',
    business_type        int                                       not null comment '商圈类型',
    region_id            int                                       not null comment '区域Id',
    partner_id           int                                       not null comment '合作商Id',
    vm_type_id           int         default 0                     not null comment '设备型号',
    vm_status            int         default 0                     not null comment '设备状态,0:未投放;1-运营;3-撤机',
    running_status       varchar(100)                              null comment '运行状态',
    longitudes           double      default 0                     null comment '经度',
    latitude             double      default 0                     null comment '维度',
    client_id            varchar(50)                               null comment '客户端连接Id,做emq认证用',
    policy_id            bigint                                    null comment '策略id',
    create_time          timestamp   default CURRENT_TIMESTAMP     not null comment '创建时间',
    update_time          timestamp   default CURRENT_TIMESTAMP     null comment '修改时间',
    constraint vendingmachine_VmId_uindex
        unique (inner_code),
    constraint tb_vending_machine_ibfk_1
        foreign key (vm_type_id) references tb_vm_type (id),
    constraint tb_vending_machine_ibfk_2
        foreign key (node_id) references tb_node (id),
    constraint tb_vending_machine_ibfk_3
        foreign key (policy_id) references tb_policy (policy_id)
)
    comment '设备表';
-- 查询并显示点位表所有的字段信息,同时显示每个点位的设备数量
SELECT
    n.id,
    n.node_name,
    n.address,
    n.business_type,
    n.region_id,
    n.partner_id,
    n.create_time,
    n.update_time,
    n.create_by,
    n.update_by,
    n.remark,
    COUNT(v.id) AS vm_count
FROM
    tb_node n
LEFT JOIN
    tb_vending_machine v ON n.id = v.node_id
GROUP BY
    n.id;
package com.dkd.manage.domain.vo;

import com.dkd.manage.domain.Node;
import com.dkd.manage.domain.Partner;
import com.dkd.manage.domain.Region;
import lombok.Data;

@Data
public class NodeVo extends Node {
    // 设备数量
    private Integer vmCount;
    // 区域信息
    private Region region;
    // 合作商信息
    private Partner partner;
}
com/dkd/manage/mapper/NodeMapper.java
/**
     *  查询点位管理列表
     * @param node
     * @return
     */
    public List<NodeVo> selectNodeVoList(Node node);
<!--
resultType="com.dkd.manage.domin.vo.NodeVo">...
这个是mybatis以前搞的自动映射封装直接把结果映射给了NodeVo的实体类了
嵌套查询就不能使用resultType自动映射 要改为resultMap做自动映射
<resultMap type="NodeVo" id="NodeVoResult">
多表查询一定要起别名噢 不然会报错没有指明where的子句是来自tb_node表的region_id  还是来自tb_vending_machine表的region_id
-->

<select id="selectNodeVoList" parameterType="Node" resultMap="NodeVoResult">
        SELECT
        n.id,
        n.node_name,
        n.address,
        n.business_type,
        n.region_id,
        n.partner_id,
        n.create_time,
        n.update_time,
        n.create_by,
        n.update_by,
        n.remark,
        COUNT(v.id) AS vm_count
        FROM
        tb_node n
        LEFT JOIN
        tb_vending_machine v ON n.id = v.node_id
        <where>
            <if test="nodeName != null  and nodeName != ''"> and n.node_name like concat('%', #{nodeName}, '%')</if>
            <if test="regionId != null "> and n.region_id = #{regionId}</if>
            <if test="partnerId != null "> and n.partner_id = #{partnerId}</if>
        </where>
        GROUP BY
        n.id
    </select>
多对一标签用association
<resultMap type="NodeVo" id="NodeVoResult">
        <result property="id"    column="id"    />
        <result property="nodeName"    column="node_name"    />
        <result property="address"    column="address"    />
        <result property="businessType"    column="business_type"    />
        <result property="regionId"    column="region_id"    />
        <result property="partnerId"    column="partner_id"    />
        <result property="createTime"    column="create_time"    />
        <result property="updateTime"    column="update_time"    />
        <result property="createBy"    column="create_by"    />
        <result property="updateBy"    column="update_by"    />
        <result property="remark"    column="remark"    />
        <result property="vmCount"    column="vm_count"    />
<!--
sql语法拿到region_id去执行区域当中的selectRegionById 方法执行的时候需要传递区域的id
原理:RegionMapper.java中的 public Region selectRegionById(Long id)把条件拿到手并封装给区域的Region对象 返回的Region对象最终映射给NodeVo.java的 private Region region;
怎么执行的映射呢?需要指定java属性名和执行的类型:
property="region" 
javaType="Region"
至此完成了mybatis的嵌套查询
-->
        <association property="region" javaType="Region" column="region_id" select="com.dkd.manage.mapper.RegionMapper.selectRegionById"/>
        <association property="partner" javaType="Partner" column="partner_id" select="com.dkd.manage.mapper.PartnerMapper.selectPartnerById"/>
    </resultMap>
<resultMap>......</resultMap>   // 完成手动映射 为了实现多表映射情况组合查询

// 解决一对一 或 多对一 映射结果集只有一个对象时完成的ORM的映射封装
<association>......</association>  #点位和点位1对1  点位和合作商1对多

// 解决一对多场景下来映射多个结果的集合 单个区域表+区域点位列表 映射的是集合!!
<collection>......</collection> 


/*
// <association> 标签允许你在查询结果中嵌套另一个对象。
 这样可以方便地在 NodeVo 对象中直接访问 Region 和 Partner 的属性,而不需要额外的查询
 
property:指定 NodeVo 类中的属性名称,该属性将引用关联的对象。
javaType:指定关联对象的 Java 类型。
column:指定用于关联查询的列名,通常是外键。
select:指定一个子查询的方法,用于根据外键查询关联对象
*/

NodeService

/**
 * 查询点位管理列表
 * @param node
 * @return NodeVo集合
 */
public List<NodeVo> selectNodeVoList(Node node);

NodeServiceImpl

/**
 * 查询点位管理列表
 *
 * @param node
 * @return NodeVo集合
 */
@Override
public List<NodeVo> selectNodeVoList(Node node) {
    return nodeMapper.selectNodeVoList(node);
}

NodeController

/**
 * 查询点位管理列表
 */
@PreAuthorize("@ss.hasPermi('manage:node:list')")
@GetMapping("/list")
public TableDataInfo list(Node node)
{
    startPage();
    List<NodeVo> voList = nodeService.selectNodeVoList(node);
    return getDataTable(voList);
}
com/dkd/manage/mapper/NodeMapper.java
/**
     *  查询点位管理列表
     * @param node
     * @return
     */
    public List<NodeVo> selectNodeVoList(Node node);

node/index.vue

<!-- 点位列表 -->
<el-table v-loading="loading" :data="nodeList" @selection-change="handleSelectionChange">
  <el-table-column type="selection" width="55" align="center" />
  <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
  <el-table-column label="点位名称" align="center" prop="nodeName" />
  <el-table-column label="所在区域" align="center" prop="region.regionName" />
  <el-table-column label="商圈类型" align="center" prop="businessType">
    <template #default="scope">
      <dict-tag :options="business_type" :value="scope.row.businessType" />
    </template>
  </el-table-column>
  <el-table-column label="合作商" align="center" prop="partner.partnerName" />
  <el-table-column label="详细地址" align="center" prop="address" show-overflow-tooltip="true"/>
  <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
    <template #default="scope">
      <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['manage:node:edit']">修改</el-button>
      <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['manage:node:remove']">删除</el-button>
    </template>
  </el-table-column>
</el-table>

区域管理改造-地区详情新增查看详情

src\views\manage\region\index.vue
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template #default="scope">
          <el-button link type="primary"  @click="getRegionInfo(scope.row)" v-hasPermi="['manage:region:list']">查看详情</el-button>
          <el-button link type="primary"  @click="handleUpdate(scope.row)" v-hasPermi="['manage:region:edit']">修改</el-button>
          <el-button link type="primary"  @click="handleDelete(scope.row)" v-hasPermi="['manage:region:remove']">删除</el-button>
        </template>
      </el-table-column>

<!-- 
template 插槽:
#default="scope":定义默认插槽,scope 是当前行的数据对象。

@click="getRegionInfo(scope.row)":点击按钮时调用 getRegionInfo 函数,并传入当前行的数据。
-->

...
...

/* 查看详情操作按钮 */
function getRegionInfo(row) {
  // 查询区域信息
  reset();
  const _id = row.id
  getRegion(_id).then(response => {
    form.value = response.data
  });
}

<!-- 
nodeList 变量:

const nodeList = ref([]):定义一个响应式数组 nodeList,用于存储点位列表。
getRegionInfo 函数:

reset():调用 reset 函数,可能用于重置表单或其他状态。
const _id = row.id:获取当前行的 id。
getRegion(_id).then(response => { form.value = response.data }):调用 getRegion 函数查询区域信息,并将返回的数据赋值给 form.value。
loadAllParams.regionId = row.id:设置 loadAllParams 对象的 regionId 属性为当前行的 id。
listNode(loadAllParams).then(response => { nodeList.value = response.rows }):调用 listNode 函数查询点位列表,并将返回的行数据赋值给 nodeList.value。
-->
区域管理里引入点位的api文件
import { listNode } from "@/api/manage/node";
import { loadAllParams } from "@/api/page";
...
/* 查看详情操作按钮 */
const nodeList = ref([]);
function getRegionInfo(row) {
  // 查询区域信息
  reset();
  const _id = row.id
  getRegion(_id).then(response => {
    form.value = response.data
  });
  // 查看点位列表
  loadAllParams.regionId=row_id
  listNode(loadAllParams).then(response => {
    nodeList.value = response.rows;
  });
}
添加区域管理对话框
 <!-- 添加或修改区域管理对话框 -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="regionRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="区域名称" prop="regionName">
          <el-input v-model="form.regionName" placeholder="请输入区域名称" />
        </el-form-item>
        <el-form-item label="备注说明" prop="remark">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="cancel">取 消</el-button>
        </div>
      </template>
    </el-dialog>
   <!-- 查看详情对话框 -->
<el-dialog title="区域详情" v-model="regionInfoOpen" width="500px" append-to-body>
    <el-form-item label="区域名称" prop="regionName">
        <el-input v-model="form.regionName" disabled />
    </el-form-item>
    <label>包含点位:</label>
    <el-table :data="nodeList">
        <el-table-column label="序号" type="index" width="50" align="center" />
        <el-table-column label="点位名称" align="center" prop="nodeName" />
        <el-table-column label="设备数量" align="center" prop="vmCount" />
    </el-table>
</el-dialog>
/* 查看详情操作按钮 */
const nodeList = ref([]);
const regionInfoOpen=ref(false);
function getRegionInfo(row) {
  // 查询区域信息
  reset();
  const _id = row.id
  getRegion(_id).then(response => {
    form.value = response.data
  });
  // 查看点位列表
  loadAllParams.regionId=row_id
  listNode(loadAllParams).then(response => {
    nodeList.value = response.rows;
  });
  regionInfoOpen.value=true;
}
数据完整性
  • 在删除区域或合作商数据时,关联的点位数据该如何处理?

tb_region(区域表) tb_node(点位表) tb_partner(合作商表)
id ←region_id:id ← region_id id
partner_id partner_id:id →→→↑

找到设置外键约束(取消约束)dkd → tb_node → Modify Table(old) 找到Foreign Keys 双击打开后将Update ruleDelete rule修改为:no action

**CASCADE(级联操作):**当父表中的某行记录被删除或更新时,与其关联的所有子表中的匹配行也会自动被删除或更新。这种方式适用于希望保持数据一致性的场景,即父记录不存在时,相关的子记录也应该被移除。

**SET NULL(设为空):**若父表中的记录被删除或更新,子表中对应的外键字段会被设置为NULL。选择此选项的前提是子表的外键列允许为NULL值。这适用于那些子记录不再需要明确关联到任何父记录的情况。

**RESTRICT(限制):**在尝试删除或更新父表中的记录之前,数据库首先检查是否有相关联的子记录存在。如果有,则拒绝执行删除或更新操作,以防止意外丢失数据或破坏数据关系的完整性。这是一种保守策略,确保数据间的引用完整性。

**NO ACTION(无操作):**在标准SQL中,NO ACTION是一个关键字,它要求数据库在父表记录被删除或更新前,检查是否会影响子表中的相关记录。在MySQL中,NO ACTION的行为与RESTRICT相同,即如果子表中有匹配的行,则禁止执行父表的删除或更新操作。这意味着如果存在依赖关系,操作将被阻止,从而保护数据的参照完整性。

修改完毕后,如果你尝试进行删除操作,会发现数据库的完整性约束生效了,它会阻止删除操作并给出错误提示。但是,这个错误提示信息可能对于用户来说不够友好,可能会让用户感到困惑。

SQLIntegrityConstraintViolationException是Java中的一个异常类,这个类通常用于表示SQL数据库操作中的完整性约束违反异常

例如:外键约束、唯一约束等。当数据库操作违反了这些约束时,就会抛出这个异常。

这个错误是由于外键约束导致的。它表明在删除或更新父表的行时,存在外键约束,子表中的相关行会受到影响。

是因为在删除tb_region表中的行时,tb_node表中的region_id外键约束会阻止操作。

如果你在使用Spring框架进行数据库操作,可能会先遇到DataIntegrityViolationException,它是对SQLIntegrityConstraintViolationException的一个更高层次的抽象,旨在提供一种更加面向应用的错误表示。

而SQLIntegrityConstraintViolationException是更底层的异常,直接来源于数据库驱动,包含更多底层数据库相关的细节。

在实际开发中,推荐捕获并处理DataIntegrityViolationException,因为它更符合Spring应用的异常处理模式,同时也可以通过其内部的cause(原因)属性来获取具体的SQLIntegrityConstraintViolationException,进而获取详细的错误信息。

为了提升用户体验,我们可以使用Spring Boot框架的全局异常处理器来捕获这些错误信息,并返回更友好的提示信息给用户。这样,当用户遇到这种情况时,他们将收到一个清晰、易懂的提示,告知他们操作无法完成的原因。

com/dkd/framework/web/exception/GlobalExceptionHandler.java
/**
     * 数据完整性异常
     */
    @ExceptionHandler(DataIntegrityViolationException.class)
    public AjaxResult handleDataIntegrityViolationException(DataIntegrityViolationException e) {
        log.error(e.getMessage(), e);
        if (e.getMessage().contains("foreign")) {
            return AjaxResult.error("无法删除该数据,有其他数据引用");
        }
        return AjaxResult.error("数据完整性异常,请联系管理员");
    }

16

阅读全文

Java面试专项

2024/10/31

Redis篇

我看你做的项目中,都用到了redis,你在最近的项目中哪些场景使用了redis呢?
  • 验证你项目场景的真实性,二是为了深入发问的切入点
  • 缓存 缓存三兄弟(穿透、击穿、雪崩)、双写一致、持久化、数据过期策略、数据淘汰策略
  • 分布式锁 setnx、redisson
  • 消息队列、延迟队列 何种数据类型

==缓存穿透==:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库(可能原因是数据库被攻击了 发送了假的/大数据量的请求url)

  • 解决方案一缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存 {key:1, value:null}
    优点:简单
    缺点:消耗内存,可能会发生不一致的问题

  • 解决方案二布隆过滤器 (拦截不存在的数据)

    在缓存预热时,要预热布隆过滤器。根据id查询文章时查询布隆过滤器如果不存在直接返回

    bitmap(位图):相当于一个以bit位为单位的数组,数组中每个单元只能存储二进制数0或1

    布隆过滤器作用:可以用于检索一个元素是否在集合中

    • 存储数据:id为1的数据,通过多个hash函数获取hash值,根据hash计算数组对应位置改为1
    • 查询数据:使用相同hash函数获取hash值,判断对应位置是否都为1

    存在误判率:数组越小 误判率越大

    bloomFilter.tryInit(size, 0.05) //误判率5%
    

==缓存击穿==:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能一瞬间把DB击穿

  • 解决方案一互斥锁【数据强一致性 性能差 (银行)】

    1.查询缓存,未命中 → 2.获取互斥锁成功 → 3.查询数据库重建缓存数据 → 4.写入缓存 → 5.释放锁

    1.查询缓存,未命中 → 2.获取互斥锁失败 → 3.休眠一会再重试 → 4.写入缓存重试 → 5.缓存命中

  • 解决方案二逻辑过期[不设置过期时间] 【高可用 性能优 不能保证数据绝对一致 (用户体验)】
    也可以搞个永不过期 具体是先在业务里写好某种情况下 某些时候不会过期 比如疫情卖口罩时期

    在数据库一条数据里面添加一个 “expire”: 153213455

    1.查询缓存,发现逻辑时间已过期 → 2.获取互斥锁成功 → 3.开启线程 ↓→ 4.返回过期数据

    ​ 【在新的线程】→ 1.查询数据库重建缓存数据 → 2.写入缓存,重置逻辑过期时间 → 3.释放锁
    1.查询数据缓存,发现逻辑时间已过期 → 2.获取互斥锁失败 → 3.返回过期数据

==缓存雪崩==:在同一个时段内大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来压力

  • 解决方案一:给不同的key的TTL(过期时间)添加随机值
  • 解决方案二:利用Redis集群提高服务的可用性 【哨兵模式、集群模式】
  • 解决方案三:给缓存业务添加降级限流策略【nginx、springcloud、gateway】
  • 解决方案四:给业务添加多级缓存 【Guava(做一级缓存 然后Redis是二级缓存)或Caffeine】
《缓存三兄弟》
穿透无中生有key,布隆过滤null隔离。
缓存击穿过期key,锁与非期解难题。
雪崩大量过期key,过期时间要随机。
面试必考三兄弟,可用限流来保底。
redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

二、Redis 和 MySQL 不一致会发生什么?

如果同步失败,会出现:

  • 缓存中的数据是脏的(脏读)
  • 缓存中没有数据,但数据库中有(缓存穿透)
  • 数据库更新了,但 Redis 仍是旧值(一致性问题)
  • 并发写操作丢失最新值(写丢失)

先插入数据库
更新先更新数据库 更新数据库成功但redis不成功 影响不大 因为后面会有过期删除 最终会一致,更新mysql后缓存可以删除也可以修改
更新完数据库直接删除缓存了 有过期时间兜底 最终会保持一致 我们项目中对数据敏感性一致性不高 我们追求实时性
如果是最终保持一致性的就MQ 我们对实时性不高 对数据敏感性 一致性高
删除问题不大 哪里都行!
读多写少的可以上缓存
mysql保存购物车表 但是再页面操作的时候 只操作redis 用mq给到消费者修改或定时任务 更新数据到mysql,MQ问题:我们对数据实时性要求不高 只需要保存最终一致性就行

你如果只写redis 万一丢了数据怎么办
购物车丢点订单无影响 数据安全性要求不太高 mysql尽量不要搞购物车的表 都在redis的表 丢就丢了呗。
或者异步同步/定时任务
实时性要求 安全性要求 → MySQL
电商一般数据库和mysql都要存 → 读多写少

一定、一定、一定要设置前提,介绍自己的业务背景 (一致性要求高?允许延迟一致?)

① 介绍自己简历上的业务,我们当时是把文章的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性并没有那么高,所以我们采用的是异步的方案同步的数据

② 我们当时是把抢卷的库存存入到了缓存中,这个需要实时的进行数据同步,为了保证数据的强一致性,我们当时采用的是redission提供的读写锁来保证数据的同步

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

  • 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间

  • 写操作:延迟双删 [因为无论先删除缓存还是先删除数据库都可能会出数据不一致问题 有脏数据]

  • ==基于redisson互斥锁:==[放入缓存中的数据 读多写少] 【强一致性业务 性能低】

    • 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,但**不允许写操作**
    • 排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作(只允许一个用户或进程独占地对数据进行读取和写入操作)排他锁确保了写操作的原子性和一致性
    • 读数据的时候添加共享锁(读不互斥、写互斥)
    • 写数据的时候添加排他锁(阻塞其他线程的读写 因为读多写少)

    redissionClient.getReadWriteLock(“xxxx”);

  • ==异步通知==: 异步通知保证数据的最终一致性(需要保证MQ的可靠性)需要在Redis中更新数据的同时,通知另一个服务进行某些操作。

    • 使用场景
      • 缓存与数据库双写: 当应用需要同时更新Redis缓存和数据库时,可以先将数据写入Redis,然后通过异步通知机制触发数据库的更新操作。
      • 跨地域数据复制: 在跨地域部署的服务中,为了实现数据的最终一致性,可以在一个地域写入数据后,通过异步通知机制在另一个地域进行数据复制。
      • 系统间数据同步: 在微服务架构中,不同的服务可能有自己的数据存储。当一个服务更新了数据后,可以通过异步通知机制告知其他相关服务进行数据同步。
  • ==基于Canal的异步通知==:监听mysql的binlog

    • 使用MQ中间件,更新数据之后,通知缓存删除
    • 利用canal中间件,不需要修改业务代码,伪装为mysqls的一个从节点,canal通过读取binlog数据更新缓存

✅ 为什么会出现 Redis 和 MySQL 数据不一致?

Redis 是缓存,MySQL 是数据库,它们的数据生命周期不同,导致在更新时会出现以下几种情况:


💥 情况1:先更新数据库,再删除缓存(常见写法)

update DB
delete cache
  • 假设刚执行完 update DB,还没来得及 delete cache,此时某个高并发请求进来:
    • 它先查缓存,发现是旧数据;
    • 然后返回了错误的数据;
    • 后续即使删了缓存,已经晚了。

这就是:缓存未及时失效,读取到了脏数据


💥 情况2:先删除缓存,再更新数据库

delete cache
update DB
  • 这个时候如果并发查询线程来得很快:
    • 缓存刚被删,查询请求就查不到;
    • 就会穿透查数据库,查到旧数据;
    • 然后又把旧数据写入了 Redis,覆盖了更新后的数据!

💥 情况3:缓存过期后查询数据库,正好遇到更新未完成

Redis key 过期
查询数据库返回旧数据
写入 Redis(错的数据被缓存)

这就是缓存击穿 + 数据同步延迟的问题。


✅ 数据为什么会“丢”?需要同步吗?

Redis 是内存数据库,不具备强一致性保障,以下情况会导致“看起来丢数据”:

  1. 更新了数据库,但缓存没更新/没删除
  2. 缓存提前过期,重新加载了旧数据
  3. Redis 重启,缓存丢失
  4. 并发穿透,旧数据反复写入缓存
  5. 程序异常,缓存更新/删除逻辑没执行

✅ 怎么保证 Redis 和 MySQL 的一致性?

这就是我们说的:缓存与数据库双写一致性问题,常见策略如下:

1️⃣ 读写操作采用延迟双删策略(推荐)

update DB
delete Redis
sleep 500ms
delete Redis again
  • 延迟双删可以尽可能避免并发查询旧缓存的情况。

2️⃣ 加分布式锁

  • 给关键资源加锁,串行化更新操作,避免并发穿透。

3️⃣ 异步更新缓存(利用消息队列)

  • 变更数据后,发送消息通知缓存异步刷新。

4️⃣ 设置合理的缓存 TTL + 定时刷新

  • 防止长期过期数据驻留,降低不一致几率。

5️⃣ 不缓存非热点数据

  • 某些冷门数据没必要缓存,避免无谓一致性维护。
Redis作为缓存,数据的持久化是怎么做的?

Redis持久化:RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,数据恢复。

[root@localhost ~]# redis-cli
127.0.0.1:6379> save          #由Redis主进程来执行RDB,会阻塞所有命令
ok

127.0.0.1:6379> bgsave        #开 启子进程执行RDB,避免主进程受到影响
Background saving started

Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:

// 900秒内,如果至少有1个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000

==RDB的执行原理?==数据完整性高用RDB
save就是直接让主线程去执行

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件

在LInux中主进程并无法直接读取物理内存,它只能通过虚拟内存去读。因此有页表(记录虚拟地址与物理地址的映射关系)去执行操作 同时 主进程也会fork(复制页表) 成为一个新的子进程(携带页表) → 写新RDB文件替换旧的RDB文件 → 磁盘

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作

优点:二进制数据重启后 Redis无需过多解析 直接恢复

==AOF==对数据不敏感要求不高

AOF全称为Append Only File(追加文件)底层硬盘顺序读写。Redis处理的每个写命令都会记录在AOF,可以看作是命令日志文件
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

AOF的命令记录的频率也可以通过redis.conf文件来配

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完毕先放入AOF缓冲区,然后表示每隔一秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 刷盘时机 优点 缺点
Always 同步刷盘 可靠性高,几乎不丢数据 性能影响大
everysec 每秒刷盘 性能适中 最多丢失1秒数据
no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义通过执行bgrewriteaof命令,可以让AOF文件执行重读功能,用最少的命令达到相同效这是AOF文件越来越大的处理方式

Redis会在出发阈值时自动重写AOF文件。阈值也可以在redis.conf中配置

# AOF文件比上次文件 增多超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

★★★★★★★★ RDB与AOF对比 ★★★★★★★★

RDB和AOF各有优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
RDB是二进制文件,在保存时体积较小恢复较快,但也有可能丢失数据,我们通常在项目中使用AOF来恢复数据,虽然慢但丢失数据风险小,在AOF文件中可以设置刷盘策略(每秒批量写入一次命令)

RDB AOF
持久化方式 定时对整个内存做快照 记录每一次执行的命令
数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
文件大小 会有压缩,文件体积小 记录命令,文件体积大
宕机恢复速度 很快
数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源
但AOF重写时会占用大量CPU和内存资源
使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见
假如Redis的key过期之后,会立即删除吗

Redis对数据设置数据的有效时间,数据过期以后就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)

==Redis数据删除策略-惰性删除==

惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key

set name zhangsan 10
get name # 发现name过期了,直接删除key

优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不会浪费时间进行过期检查
缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

==Redis数据删除策略-定期删除==

定期删除:每隔一段时间,我们就会对一些key进行检查,删除里面过期的key (从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)

定期清理的两种模式:

  • SLOW模式是定时模式,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数
  • FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存
难点:难以确定删除操作执行的时长和频率

Redis过期删除策略: 惰性删除 + 定期删除 两种策略进行配合使用

假如缓存过多,内存是有限的,内存被占满了怎么办?

==数据淘汰策略==

当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据制除掉,这种数据的制除规则被称之为内存的淘汰策略

Redis支持8种不同策略来选择要删除的key:

  • noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略

    maxmemory-policy noeviction
  • volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰 (TTL:过期时间的key)

  • allkeys-random:对全体key,随机进行淘汰

  • volatile-random:对设置了TTL的key,随机进行淘汰

  • allkeys-lru:对全体key,基于LRU算法进行淘汰

    LRU(Least Recently Used):最近最少使用,用当前时间减去最后一次访问时间,这个值越大测淘汰优先级越高 [逐出访问时间最少的]
    LFU(Least Frequently Used):最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。[逐出频率最低的] 【电商会应用】

  • allkeys-lfu:对全体key,基于LFU算法进行淘汰

  • volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰

淘汰策略 - 使用建议

1.优先使用 allkeys-lru 策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中,如果业务有明显的冷热数据区分,建议使用。
2.如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random,随机选择淘汰
3.如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不会被删除,会淘汰其他设置过期时间的数据
4.如果业务中有短时高频访问的数据,可以使用allkeys-lfuvolatile-lfu策略

数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?
  • 使用allkeys-lru(挑选最近最少使用的数据淘汰) 淘汰策略,留下来的都是经常访问的热点数据
Redis的内存用完了会发生什么?
  • 主要看数据淘汰策略是什么?如果是默认的配置(noeviction),会直接报错
redis分布式锁,是如何实现的?

需要结合项目中的业务进行回答,通常情况下,分布式锁的使用场景:
集群情况下的定时任务、抢单、幂等性场景
如果使用互斥锁的话 那么在集群项目有多个服务器就会出现问题

==Redis分布式锁==

Redis实现分布式锁主要利用Redis的setnx命令,setnx是**SET if not exists**(如果不存在,则SET)的简写
在同一时刻,只有一个线程/进程/服务节点能拿到锁,执行关键代码。其他的只能等或者失败退出。

  • 获取锁

    添加锁,NX是互斥、PX是设置超时时间
    SET lock value NX PX 10

  • 释放锁

    释放锁,删除即可
    DEL key

Redis实现分布式锁如何合理的控制锁的有效时长?
  • 根据业务执行时间预估
  • 给锁续期
你“自己用 Redis 实现分布式锁” vs 用 Redisson 实现锁 —— 有哪些区别?
对比点 手动实现(自己用 Redis 命令) ✅ Redisson 实现
🛠 实现方式 使用 SET key value NX PXGET + DEL 内部封装好,直接调用 .lock().unlock()
🔐 安全性 容易写错,不易保证“只有加锁者能解锁 Redisson 自动做到了“加锁者才能解锁”
💣 死锁处理 自己必须手动加过期时间(PX),否则会死锁 自动设置超时时间,支持 watchdog 自动续命
🧵 可重入锁 不支持,需要自己实现复杂逻辑 ✅ 内建支持可重入锁(ReentrantLock)
⚠ 主从不一致 Redis 主从复制延迟下可能“锁丢失” Redisson 有 RedLock 模式,可用多个实例容错
🚦 阻塞等待 需要自己写轮询逻辑(比如 while循环) ✅ Redisson 支持阻塞、等待、超时等参数
⏱ 自动续租 没有,需要自己定时续约 ✅ 有“看门狗机制”自动续租防止业务太长释放锁
🔁 分布式支持 支持有限,自己写 RedLock 非常麻烦 ✅ 内置 RedLock、联锁、多锁等高级功能
✅ 使用体验 复杂,代码易错 ✅ 简单、线程安全、功能全面

==redisson实现分布式锁 - 执行流程==
✅ 只有在 你没指定超时时间 时,Watchdog 才会自动开启

加锁 ↓→ 加锁成功 → Watch dog(看门狗)每隔(releaseTime/3的时间做一次续期) → Redis
↓ 操作redis → Redis
↓→→ 释放锁↑ → 通知看门狗无需继续监听 → Redis

加锁 → → → 是否加锁成功?→→→ ↓
↑←←while循环不断尝试获取锁←←←↓

public void redisLock() throws InterruptedException{
    RLock lock = redissonClient.getLock("heimalock");
 // boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 如果不设置中间的过期时间30 才会触发看门狗
// 加锁,设置过期时间等操作都是基于lua脚本完成的[调用redis命令来保证多条命令的原子性]
    boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
    if(isLock){
        try{
            sout("执行业务");
        } finally{
            lock.unlock();
        }
    }
}
要加依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

❌ 如果你这样写:

lock.lock(10, TimeUnit.SECONDS);

就不会有自动续命,看门狗不会工作。这种锁就是严格 10 秒后自动释放,不管你业务是否完成。

🔄 Watchdog 自动续命流程图

lock.lock() 被调用
     ↓
Redisson 设置锁为30秒过期
     ↓
启动看门狗线程,每10秒刷新一次锁 TTL
     ↓
如果线程还持有锁 → Redis.expire(lockKey, 30秒)
     ↓
直到 unlock() 调用 → 结束看门狗 + 删除锁

==redisson实现分布式锁 - 可重入==

redis实现分布式锁是不可重入的 但是 redisson实现分布式锁是可以重入的
可重入原理:它俩是同一个线程 每个线程都有唯一的线程id 根据线程id唯一标识做判断 判断之前获取锁是不是同一个线程
利用hash结构记录线程id重入次数

KEY VALUE VALUE
field value
heimalock thread1 0
public void add1(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
  add2();
// 释放锁
  lock.unlock();
}
public void add2(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
// 释放锁 锁次数-1不完全释放
  lock.unlock();
}

==redisson实现分布式锁 - 主从一致性==

Redis Master主节点:主要负责 写操作(增删改) 只能写
Redis Slave从节点:主要负责读操作只能读

当RedisMaster主节点突然宕机后 Java应用会去格外获取锁 这时两个线程就同时持有一把锁 容易出现脏数据
怎么解决呢?

  • RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1),避免在一个redis实例【实现复杂、性能差、运维繁琐】怎么解决?→ CP思想zookeeper
Redis集群有哪些方案?
  • ==主从复制==

    单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
    主节点写操作→增删改 从节点读操作→查

    介绍一下redis的主从同步

    单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就要搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据

    主从数据同步原理:

    • 主从全量同步

    slave从节点执行replicaof命令建立链接 → 请求master主节点数据同步(replid+offset) → master判断是否是第一次同步(判断replid是否一致) → 是第一次, 返回master的数据版本信息(replid+offset) → slave保存版本信息 → master执行bgsave, 生成RDB → 发送RDB文件给slave → slave清空本地数据加载RDB数据 → 此时master记录RDB期间所有命令repl_balklog → 发送repl_backlog中的命令 → slave执行接收到的命令

    Replication ld: 简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
    offset: 偏移量,随着记录在repl baklog中的数据增多而逐渐增大。save完成同步时也会记录当前同步的offset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

    简述全量同步的流程?

    • slave节点请求增量同步

    • master节点判断replid,发现不一致,拒绝增量同步

    • master将完整内存数据生成RDB,发送RDB到slave

    • slave清空本地数据,加载master的RDB

    • master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave

    • slave执行接收到的命令,保持与master之间的同步

    能说一下,主从同步数据的流程吗?

    全量同步

    1.从节点请求主节点同步数据(replication id、offset)
    2.主节点判断是否为第一次请求,是第一次就与从节点同步版本信息(replication id和offset)
    3.主节点执行bgsave, 生成RDB文件后, 发送给从节点去执行
    4.在RDB生成执行期间, 主节点会从命令的方式记录到缓冲区(日志文件)

    • 主从增量同步
      主从增量同步(slave重启或后期数据变化)

    ① slave重启后 → 携带(replid+offset)找master → master判断请求replid是否一致 → 是第一次, 返回主节点replid和offset → 保存版本信息
    ② slave重启后 → 携带(replid+offset)找master → master判断请求replid是否一致 → 不是第一次, 回复continue向slave → master 去repl_baklog中获取offset后的数据 → 发送offset后的命令给slave → 执行命令

    增量同步

    1.从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
    2.主节点从命令日志中获取offset值后的数据,发送给节点进行数据同步

简述全量同步和增量同步区别?

•全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。

•增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave

什么时候执行全量同步?

•slave节点第一次连接master节点时

•slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步?

•slave节点断开又恢复,并且在repl_baklog中能找到offset时

  • ==哨兵模式==搭过集群,具体多少个节点是组长那边,不太清楚[并发量不是太多 搭哨兵可以节省一点资源]~
    Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复

    • 监控:Sentinel会不断检查您的master和slave是否按预期工作
    • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为一个master。当故障实例恢复后也以新的master为主
    • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

    服务状态监控
    Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令 期待回复pong

    • 主观下线:如果某sentinel节点发现或某实例未在规定时间相应,则认为该实例主观下线
    • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线,quorum值最好超过Sentinel实例数量的一半

    哨兵选主规则

    • 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
    • 然后判断从节点的slave-priority值,越小优先级越高
    • 如果slave-priority值一样,则判断slave节点的offset值,越大优先级越高 (数据是最全的)
    • 最后是判断slave节点的运行id大小,越小优先级越高

    redis集群(哨兵模式) 脑裂
    因网络问题 主节点和从节点分别在不同的网络分区 这样sentinel只会监控到一部分从节点网络分区 导致RedisClient继续写主节点的数据,这时网络恢复了,哨兵会将老的master强制降级到slave(携带着脑裂前的最新数据),这个时候slave就会把自己数据清空去同步master数据,这时就存在真正的数据丢失了

    怎么解决

    redis中有两个配置参数:【若不能达成就拒绝客户端请求 这样就会避免大量数据丢失】
    min-replicas-to-write 1 表示最少的salve节点为1
    min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒
    达不到这两个条件就拒绝写入,从而避免数据丢失。

    怎么保证Redis的高并发高可用呢?

    哨兵模式:实现主从集群的自动故障恢复(监控、自动故障恢复、通知)

    你们使用redis是单点还是集群,哪种集群?

    主从(1主1从) + 哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点

    redis集群脑裂,该怎么解决?

    集群脑裂是由于主节点和从节点和sentinel处于不同网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当为网络恢复后,sentinel会将老的主节点降为从节点,此时再从新master同步数据,就会导致数据丢失
    解决:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,这样就会避免大量数据丢失。

  • ==分片集群==

    主从和哨兵可以解决高可用、高并发读的问题,但是依然有两个问题没有解决:

    • 海量数据存储问题
    • 高并发写的问题

    使用分片集群可用解决上述问题,分片集群特征:

    • 集群中有多个master,每个master保存不同数据
    • 每个master都可用有多个slave节点
    • master之间通过ping监测彼此健康状态
    • 客户端请求可用访问集群任意节点,最终都会被转发到正确节点

    分片集群结果 - 数据读写

    Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希值,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽

    存数据流程:
    set name itheima → CRC16计算name的hash值(666666) → 666666%16384=11306 → 根据11306找寻所对应哈希槽的范围 并且插入数据

    redis的分片集群有什么用?

    • 集群中有多个master,每个master保存不同数据。(解决高并发的问题)
    • 每个master都可以有多个slave节点。(解决高并发的问题)
    • master之间通过ping监测彼此健康状态
    • 客户端请求可用访问集群任意节点,最终都会被转发到正确节点

    redis的分片集群中数据是怎么存储和读取的?

    • Redis 分片集群引入了哈希槽的概念,Redis 集群有16384个哈槽
    • 将16384个插槽分配到不同的实例
    • 读写数据:根据key的**有效部分**计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为播槽,寻找插所在的实例

🚀 Redis 集群常见三种方式(解决不同问题)

模式 主要解决问题 特点
① 主从复制(+读写分离) 读写压力分担 一主多从,主写从读,不能自动故障转移
② 哨兵模式(Sentinel) 高可用(自动故障转移) 在主从基础上,Sentinel 实现监控、自动选主、通知客户端
③ 分片集群(Cluster) 海量数据、高写吞吐 数据分片+多主多从,每个主分管部分槽位(16384 slots)

Redis是单线程的,但是为什么还那么快

  • Redis是纯内存操作,执行速度非常快
  • 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
  • 使用I/O多路复用模型,非阻塞IO

解释一下I/O多路复用模型?

Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求

  • 是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源,目前的I/O多路复用都是采用的epol模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要换个Socket来判断是否就绪,提升了性能

  • Redis网络模型:

    就是使用I/O多路复用结合事件的处理器来应对多个Socket请求

    • 连接应答处理器

    • 命令回复处理器,在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件

    • 命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

  • ==用户空间和内核空间==

    • Linux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间
    • 用户空间只能执行受限的命令Ring3,而且不能直接调用系统资源必须通过内核提供的接口来访问
    • 内核空间可以执行特权命令Ring0,调用一切系统资源

    Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区

    • 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
    • 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
  • 常见的IO模型

    • ==阻塞IO==

      阻塞IO就是两个阶段都必须阻塞等待:
      阶段一:

      • 用户进程尝试读取数据(网卡数据等)
      • 此时数据尚未到达,内核需要等待数据
      • 此时用户进程也处于阻塞状态

      阶段二:

      • 数据到达并拷贝到内核缓冲区,代表已就绪
      • 将内核数据拷贝到用户缓冲区
      • 拷贝过程中,用户进程依然阻塞等待
      • 拷贝完成,用户进程解除阻塞,处理数据
    • ==非阻塞IO==

      阶段一

      • 用户进程尝试读取数据(比如网卡数据)
      • 此时数据尚未到达,内核需要等待数据
      • 返回异常给用户进程
      • 用户进程拿到error后,再次尝试读取
      • 循环往复,直到数据就绪

      阶段二:

      • 将内核数据拷贝到用户缓冲区
      • 拷贝过程中,用户进程依然阻塞等待
      • 拷贝完成,用户进程解除阻塞,处理数据
    • ==IO多路复用==

      是利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源

      IO多路复用是利用单个线程来同步监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。不过监听Socket的方式、通知的方式又有多种实现

      • select
      • poll
      • epoll

      差异:
      ★ select和polI只会通知用户进程有Socket就绪,但不确定具体是哪个Socket,需要用户进程逐个历Socket来确认
      ★ epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,

      阶段一:

      • 用户进程调用select,指定要监听的Socket集合
      • 内核监听对应的多个socket
      • 任意一个或多个sacket数据就绪则返回readable
      • 此过程中用户进程阻塞

      阶段二:

      • 用户进程找别就格的socket
      • 依次调用recvfrom读取数据
      • 内核将数据拷贝到用户空间
      • 用户进程处理数据
  • Redis网络模型

MySQL篇

在MySQL中,如何定位慢查询?

1.介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概5秒钟)
2.我们系统中当时采用了运维工具(Skywalking),可以监测出哪个接口,最终因为是sql的问题
3.在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段)

产生原因:

  • 聚合查询
  • 多表查询
  • 表数据量过大查询
  • 深度分页查询

方案一:==开源工具==[调试阶段才会开启 生产阶段不会开启]

  • 调试工具Arthas
  • 运维工具:Prometheus、Skywalking(接口访问时间)

方案二:==MySQL自带慢日志==

慢查询日志记录了所有执行时间超过指定参数(long_query_time, 单位:秒,默认10秒)的所有SQL语句的日志,如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置信息:

# 开启MySQL慢日志查询开关
slow_query_log = 1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会被视为慢查询,记录慢查询日志
long_query_time = 2

那这个SQL语句执行很慢,如何分析呢?

可以采用MySQL自带的分析工具 explain

  • 通过keykey_len检查是否命中了索引(索引本身存在是否有失效的情况)
  • 通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描全盘扫描
  • 通过extra建议判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复

产生原因:

  • 聚合查询 → 新增临时表的数据
  • 多表查询 → 优化SQL语句结构
  • 表数据量过大查询 → 添加索引
  • 深度分页查询
一个SQL语句执行很慢,如何分析?

可以采用EXPLAIN或者DESC命令获取MySQL如何执行SELECT语句的信息

# 直接在select语句之前加上关键字 explain/desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;

mysql > explain select * from t_user where id = ‘1’

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t_user NULL const PRIMARY PRIMARY 98 const 1 100.00 NULL
  • possible_key:当前sql可能会使用到的索引
  • key:当前sql实际命中的索引 通过它俩查看是否可能会命中索引
  • key_len索引占用的大小 通过它俩查看是否可能会命中索引
  • Extra:额外的优化建议 看是否走过覆盖索引或回表查询
Extra 含义
Using where; Using Index 查找使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据
Using index condition 查找使用了索引,但是需要回表查询数据
  • type:这条sql的连接的类型,性能由好到差为
    • NULL
    • system:查询系统中的表
    • const:根据主键查询
    • eq_ref:主键索引查询或唯一索引查询
    • ref:索引查询
    • range:范围查询
    • index:索引树扫描
    • all:全盘扫描

了解过索引吗?(什么是索引)

索引(index)是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护着满足特定查找算法的数据结构**(B+树)**,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引

  • 索引(index)是帮助MySQL高效获取数据的数据结构(有序)
  • 提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
  • 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗

索引的底层数据结构了解过吗?

MySQL的InnoDB引擎采用的B+树的数据结构来存储索引

  • 阶数更多,路径更短
  • 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据
  • B+树便于扫库和区间查询,叶子节点是一个双向链表

**MySQL默认使用的索引底层数据结构是B+树**。再聊B+树之前,先来聊聊二叉树和B树

==B Tree(矮胖树)== B树是一种多叉路衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key

==B+Tree== 是再BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是B+Tree实现其索引结构

B树与B+树对比

  • 磁盘读写代价B+树更低
  • 查询效率B+树更加稳定
  • B+树便于扫库和区间查询

B树要找12 首先找38 左面小 再去缩小范围16和29 找到12 → 但是我们只想要12的数据 B树会额外的把38,16,29的数据全查一遍最后才到12的数据

B+树是在叶子节点才会存储数据,在非叶子节点全是指针,这样就没有其他乱七八糟的数据影响 。且查找路径是差不多的,效率较稳定

便于扫库:比如我们要查询6-34区间的数据,先去根节点扫描一次38 → 16-29 → 由于叶子节点之间有双向指针,就可以一次性把所有数据都给拿到[无需再去根节点找一次]

什么是聚簇索引?什么是非聚簇索引(二级索引)?什么是回表?

  • 聚簇索引(聚集索引)数据索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个
  • 非聚簇索引(二级索引)数据索引分开存储,B+树的叶子节点保存对应的主键,可以有多个
  • 回表查询:通过二级索引找到对应的主键值,到聚集索引中查找正行数据,这个过程就是回表
分类 含义 特点
==聚集索引(Clustered Index)== 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 必须有, 而且只有一个
==二级索引(Secondary Index)== 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 可以存在多个

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引
  • 如果不存在主键,将使用第一个唯一 (UNIQUE) 索引作为聚集索引
  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引

聚集索引和非聚集索引的具体区别

特点 聚集索引 非聚集索引
数据存储方式 数据与索引存储在一起,叶子节点存储整行数据。 数据与索引分开存储,叶子节点存储指针或主键值。
物理排序 数据按索引键的顺序物理排序。 数据的物理顺序与索引无关。
索引数量 一个表只能有一个聚集索引。 一个表可以有多个非聚集索引。
查询性能 查询效率高,尤其是范围查询和排序操作。 查询效率相对较低,可能需要“回表”操作。
更新操作影响 插入、删除或更新数据可能需要重新排序。 更新操作影响较小,仅修改索引和指针。
适用场景 范围查询、排序、分组等操作频繁的列。 查询条件筛选、快速定位数据的列。

==回表查询==

select * from user where name = 'Arm';

知道什么叫覆盖索引吗?

覆盖索引是指查询使用了索引,返回的列,必须在索引中全部能够找到

  • 使用id查询,直接走聚集索引查询,一次索引描述,直接返回数据,性能高
  • 如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用 **select *** [除非用的聚簇索引(主键)]

==覆盖索引==是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到

id name gender createdate
2 Arm 1 2021-01-01
3 Lily 0 2021-05-04
5 Rose 0 2021-04-21
6 Zoo 1 2021-07-31
8 Doc 1 2021-02-26
11 Lee 1 2021-09-11
  • id为主键,默认是主键索引
  • name字段为普通索引
select * from tb_user where id = 1;                     【覆盖索引】
select id, name from tb_user where name = 'Arm'         【覆盖索引】
select id, name, gender from tb_user where name = 'Arm' 【非覆盖索引】(需要回表查询)

MySQL超大分页怎么处理?

问题:再数据量比较大时,limit分页查询,需要对数据进行排序,效率低
解决方案:可以用覆盖索引 + 子查询处理
[我们先分页查询获取表中的id 并且对表的id进行排序 就能筛选出分页后的id集合(因为id是覆盖索引效率高) 最后再根据id集合到原来的表中做关联查询就可以得到提升了]

在数据量比较大时,如果用limit分页查询,在查询时,越往后,分页查询效率越低

mysql > select * from tb_sku limit 0,10;
10 rows in set (0.00 sec)

mysql > select * from tb_sku limit 9000000,10;
10 rows in set (11.05 sec)

因为,当在进行分页查询时,如果执行 limit 9000000,10,此时需要MySQL排序前9000010记录,仅仅返回9000000 - 9000010 的记录,其他记录丢失,查询排序的代价非常大。

==MySQL超大分页查询优化思路==:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引子查询形式进行优化

# 超大分页处理:先通过覆盖索引找到符合条件的id,再通过这个id的覆盖索引查询到所有的列
select * 
from tb_sku t,
(select id from tb_sku order by id limit 9000000,10) a
where t.id = a.id

# 10 rows in set (7.15 sec)

索引创建原则有哪些?

数据量较大,且查询比较频繁的表
常作为查询条件、排序、分组的字段
③ 字段内容区分度高
④ 内容较长,使用前缀索引
尽量联合索引
要控制索引的数量
⑦ 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它

  • 先陈述自己再实际工作中是怎么用的
  • 主键索引
  • 唯一索引
  • 根据业务创建的索引(复合索引)
创建索引的方式1
① SQL的方式
ALTER TABLE user_innodb ADD INDEX idx_name(name)

② 在建表的时候 去指定索引
...
PRIMARY KEY('id'),
KEY 'idx_name' ('name') USING HASH

③ 通过界面化工具去指定索引
字段旁边有个`索引` 可以去添加

=============================================
单个字段的索引 → 单列索引
多个字段的索引 → 联合索引
索引的类型

索引可以增加查询速度 同时也增加了更新/修改速度因为更新的第一步就是查询

普通索引 经过特殊设计的数据结构
唯一索引 唯一约束
[索引必须是唯一的 比如name就不行 因为名字可以很多建立普通索引]
主键索引 在主键索引上添加了非空约束
全文索引 一般使用搜索引擎,因为对中文的搜索不太友好美国英文开发的
[特殊的sql:select * from 表名 where match(字段名) against(‘马士兵教育’ IN NATURAL LANGUAGE MODE);]

AVL树 右右型左旋 左子树与右子树的深度差绝对值不超过1
树的节点里应该放:键值+Value值+左右子树的地址left+right
Innodb一次会加载16k(16384字节=Redis的槽位) 内存到内存
不选红黑树是因为它是二叉的,我们需要多叉树
要用==B+树==全盘扫描能力更强 叶子节点是双向链表
因为稳定性比较好 B树非所见所得 B+树是稳定几层的查找数据因为数据都在最后一层叶子节点上
Innodb的索引方法是BTREE 不能改成HASH

**数据结构可视化网**:Data Structure Visualization

  • 针对数据量较大,且查询比较频繁的表建立索引。单表超过10万数据(增加用户体验)
  • 针对常作为查询条件(where)、排序(order by)、分组(group by) 操作的字段建立索引
  • 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高 (比如address都在北京市)
  • 如果是字符串类型的字段,字段的长度越长(描述信息…),可以针对于字段的特点,建立前缀索引
  • 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引(避免回表),节省存储空间,提高查询效率
  • 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率
  • 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。

什么情况下索引会失效?

  • 违反最左前缀法则
  • 范围查询右边的列,不能使用索引
  • 不要在索引列上进行运算操作,索引将失效
  • 字符串不加单引号,造成索引失效。(类型转换)
  • 以**%开头的Like模糊查询**,索引失效
    [不影响正常查询业务 但未运用超大分页查询优化 会导致索引失效]

怎么哪块读判断索引是否失效了呢

# 执行计划explain

【2024最新版MySQL索引讲解!一个视频带你彻底搞懂MySQL索引!!【马士兵】】https://www.bilibili.com/video/BV17z421i7Kb?vd_source=5966d6c3cf3709c10b3c53b278b0f4d3

什么情况下索引会失效?
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。匹配最左前缀法则,走索引:

谈谈你对sql的优化经验?

  • 表的设计优化,数据类型的选择
  • 索引优化,索引创建原则
  • sql语句优化,避免索引失效,避免使用select
  • 主从复制、读写分离,不让数据的写入,影响读操作
  • 分库分表
  • 表的设计优化(参考阿里开发手册《嵩山版》)

    • 比如设置合适的数值(tinyint、int、bigint) ,要根据实际情况选择
    • 比如设置合适的字符串类型(char和varchar) char定长效率高,varchar可变长度,效率低

    候选人: 这个我们主要参考的阿里出的那个开发手册《嵩山版》,就比如,在定义字段的时候需要结合字段的内容来选择合适的类型,如果是数值的话,像tinyint、int、bigint这些类型,要根据实际情况选择。如果是字符串类型,也是结合存储的内容来选择char和varchar或者text类型

  • 索引优化(参考优化创建原则和索引失效)

  • SQL语句优化

    • SELECT语句务必指明字段名称 (避免直使用select *)回表

    • SQL语句要避免造成索引失效的写法

    • 尽量使用union all代替union,union(不会重复)会多一次过滤, 效率低

      select * from t_user where id > 2
      union all | union
      select * from t_user where id < 5
      
    • 避免在where子句中对字段进行表达式操作

    • join优化 能用inner join 就不用left join, right 如必须使用 一定要以小表为驱动;内链接会对两个表进行优化,优先把小表放到外边,把大表放到里边。left join 或 right join,不会重新调整顺序

      for(int i = 0; i < 3; i++){ //只链接查询3次
       for(int j = 0; j < 1000; j++){
      
       }  
      }
      
  • 主从复制、读写分离(在生产环境下一般会搭建主库和从库 分开读操作和写操作)

    如果数据库的使用场景读的操作比较多的时候,为了避免写的操作所造成的性能影响 可以采用读写分离的架构。读写分离解决的是,数据库的写入,影响了查询的效率。[Master(写) 和 Slave(读)]

  • 分库分表(后面有介绍)

事务的特性是什么?可以详细的说一下吗?【ACID】

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

候选人:嗯,这个比较清楚,ACID,分别指的是:原子性、一致性、隔离性、持久性;
我举个例子:A向B转账500,转账成功,A扣除500元,B增加500元。
原子性操作体现在要么都成功,要么都失败
在转账的过程中,数据要一致性,A扣除了500,B必须增加500
在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰
在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)

  • **原子性(**Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
  • 隔离性(lsolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境运行
  • 持久性(Durabiity):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

并发事务带来哪些问题?怎么解决这些问题?MySQL默认隔离级别是?

  • ==并发事务问题==:脏读、不可重复读、幻读
  • ==隔离级别==:读未提交、读已提交、可重复读、串行化
问题 描述
脏读 一个事务读到另外一个事务还没有提交的数据
不可重复读 一个事务先后读取同一条事务,但两次读取的数据不同,称之为不可重复读
幻读 一个事务按照条件查询数据时,没有对应的数据行,这同时另一个事务B(insert且commit)了事务,此时事务A在插入数据时候,又发现这行数据已经存在了,好像出现了”幻影“
怎么解决并发事务的问题呢??

对事务进行隔离 (× 是代表可以解决此问题)

隔离级别 脏读 不可重复读 幻读
Read uncommitted 未提交读
Read committed 读已提交 ×
==Repeatable Read(默认) 可重复读== × ×
Serializable 串行化 × × ×

注意:**事务隔离级别越高,数据越安全,但是性能越低**

数据库的undo log 和 redo log的区别?

redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
redo log 保证了事务的持久性,undolog保证了事务的原子性和一致性

redo logundo log 是 InnoDB 为了实现事务的 原子性持久性 而设计的两种日志机制:

日志类型 作用 类比
redo log 崩溃恢复,重做操作(实现持久性)【恢复】 保存键 Ctrl+S
undo log 回滚事务,撤销操作(实现原子性)【撤销】 撤销键 Ctrl+Z
  • 缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改査操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
  • 数据页(page):是InnoD8 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。页中存储的是行数据

假设你执行一条 SQL:

UPDATE account SET balance = balance - 100 WHERE id = 1;

InnoDB 的执行顺序是:

  1. 写入 undo log(记录原值,便于回滚) ✅
  2. 修改内存中的数据(Buffer Pool)✅
  3. 写入 redo log(记录新的值,崩溃后可重做)✅
  4. 提交事务时将 redo log 落盘(刷到磁盘)
🔄 崩溃恢复时如何用它们?
  • 💥 宕机恢复(crash recovery)时,MySQL 会用 redo log 把“已提交但还没写到磁盘的数据”重做一遍,确保数据不丢失(持久性)。
  • 事务失败或回滚时,MySQL 用 undo log 把数据恢复到修改之前的样子,确保事务“要么全做,要么全不做”(原子性)。

==redo log==

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
该日志文件由两部分组冲:重做日志缓冲(redo log buffer) 以及 **重做日志文件(redo log file)**,前者是在内存中,后者是在磁盘中。当事务提交之后会把所有修改信息都保存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。

==undo log==

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚MVCC(多版本并发控制)。undolog 和 redolog记录物理日志不一样,它是逻辑日志

  • **可以认为当delete一条记录时,undo log中会记录一条对应的insert记录**,反之亦然
  • 当update一条记录时,它记录一条对应相反的update记录。当执行rolback时,就可以从undolog中的逻辑记录读取到相应的内容并进行回滚。

undo log可以实现事务的一致性和原子性

事务中的隔离性是如何保证的呢?

事务的隔离性主要是通过锁机制MVCC(多版本并发控制) 来实现的。

对于更新操作 (写),MySQL 会使用加锁机制,比如行级锁中的排他锁(X锁)来避免并发写冲突;
对于查询操作 (读),MySQL 使用 MVCC 来避免加锁带来的性能开销,从而支持高并发读操作。
MVCC 的核心思想是:
为同一条数据维护多个版本
,从而实现 “读写不冲突、并发更高效”。

排他锁 (如果一个事务获取到了一个数据行的排他锁,其他事务就不能再获取该行的其他锁)
mvcc: 多版本并发控制 让MySQL中的多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突
隐藏字段:
每一行数据都会有两个隐藏字段
① trx _id(事务id),记录每一次操作的事务id,是自增的 [当前这条记录是由哪个事务创建的(事务ID)]
② roll_pointer(回滚指针),指向上一个版本的事务版本记录地址(形成一个版本链)

undo log:
① 回滚日志,存储老版本数据
② 版本链:多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过rollpointer指针形成一个链表

readView:解决的是一个事务查询选择版本的问题
根据readView的匹配规则和当前的一些事务id判断该访问那个版本的数据》不同的隔离级别快照读是不一样的,最终的访问的结果不一样RC:每一次执行快照读时生成ReadView
RR:仅在事务中第一次执行快照读时生成ReadView,后续复用

面试官: 事务中的隔离性是如何保证的呢?(你解释一下MVCC)
候选人: 事务的隔离性是由锁和mvcc实现的。
其中mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undolog日志,第三个是readView读视图
隐藏字段是指:在mysq!中给每个表都设置了隐藏字段,有一个是x_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll-pointer(回滚指针),指向上一个版本的事务版本记录地址
undolog主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行探作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
readview解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是r隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用

MVCC底层的三个关键机制

🔹 1. 隐藏字段

每一行数据都会有两个隐藏字段:

  • trx_id:当前这条记录是由哪个事务创建的(事务ID)
  • roll_pointer:回滚指针,指向这条记录的上一个版本(形成一个版本链)

🔹 2. undo log(回滚日志)

  • 当事务对数据进行修改时,会记录修改前的旧数据到 undo log
  • 所有旧版本数据通过 roll_pointer 串成一个“版本链”
  • 查询时可以根据版本选择合适的数据版本,从而“读老数据”

🔹 3. ReadView(读视图)

  • 在执行快照读时,InnoDB 会生成一个 ReadView
  • 它记录了当前活跃的事务ID列表,以及当前事务的ID
  • 查询时,会根据 ReadView 判断:这条记录版本是否“可见”

总的来说,写操作靠加锁,读操作靠 MVCC。MVCC 通过维护多个版本的数据 + ReadView 机制,让不同事务之间在查询时互不干扰,从而保证隔离性,同时提升并发性能。

🔐 写用锁、📚 读用 MVCC,🔁 多版本 + 🔍 读视图 + 🧾 回滚日志,性能高,隔离强!

解释一下MVCC?

全程 Multi-Version Concurrency Control多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突

问题的来源:(橙色的)查询的是哪个事务版本的记录?
事务2 事务3 事务4 事务5
开始事务 开始事务 开始事务 开始事务
修改id为30记录, age改为3 查询id为30的记录
提交事务
修改id为30记录, name改为A3
查询id为30的记录
提交文件 修改id为30的记录, age改为10
查询id为30的记录 查询id为30的记录
提交事务
MVCC-实现原理
  • 记录中的隐藏字段
id age name DB_TRX_ID DB_ROLL_PTR DB_ROW_ID
  • DB_TRX_ID:最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID
  • DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo log, 指向上一个版本
  • DB_ROW_ID:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段
undo log
  • 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志相反的语句
  • 当insert的时候,产生的undolog日志只在回滚时需要,在事务提交后,可被立即删除。
  • 而update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除。
undo log版本链

不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录

  • readview

ReadView(读视图) 是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id

ReadView中包含了四个核心字段

字段 含义
m_ids 当前活跃的事务ID集合
min_trx_id 最小活跃事务ID
max_trx_id 预分配事务ID, 当前最大事务ID+1 (事务ID是自增的)
creator_trx_id ReadView创建者的事务ID
  • 当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select .. lock in share mode(共享锁),select .. for update、update、insert、delete(排他锁)都是一种当前读。

  • 快照读

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
Read Committed:每次select,都生成一个快照读。
Repeatable Read:开启事务后第一个select语句才是快照读的地方。

MySQL主从同步原理?

主从同步就是主库把所有数据修改写到日志文件(Binlog)里,从库再去“抄作业”——读日志,写自己,中继日志相当于中转站,最终把主库的修改同步过来。

MySQL主从复制的核心就是二进制日志binlog[DDL(数据定义语言)语句DML(数据操纵语言)语句]
主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
从库读取主库的二进制日志文件 Binlog,写入到从库的中继日志 Relay Log
从库重做中继日志中的事件,将改变反映它自己的数据。


MySQL 主从同步分三步:

  • 写日志:主库提交事务 → 写 Binlog
  • 拉日志:从库 I/O 线程拉取 Binlog → 写 Relay Log
  • 执行日志:从库 SQL 线程执行 Relay Log → 同步数据

整个过程就是:主库写 → 从库拉 → 从库执行

MySQL主从复制的核心就是二进制日志

二进制文件(BINLOG) 记录了所有的DDL(数据定义语言)语句DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句

       +---------------------+
       |      Master 主库     |
       | 写 Binlog(记录变更) |
       +---------------------+
                  ↓
       [ I/O 线程拉取 Binlog ]
                  ↓
       +---------------------+
       |      Slave 从库      |
       | 写 Relay Log(中继) |
       | 执行日志 → 同步数据  |
       +---------------------+
复制分成三步:
  • Master主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中
  • 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log
  • slave重做中继日志中的事件,将改变反应他自己的数据

你们项目用过分库分表吗?

是的,我们项目在业务数据达到一定规模后使用了分库分表策略。
具体来说,我们在做【举个真实业务:比如订单系统、日志存储系统、会员行为分析系统】时,由于单表数据量突破了 1000W+,单表容量超过 20GB,查询响应变慢,索引命中率降低,磁盘和网络 I/O 成为瓶颈。
一开始我们尝试通过
主从读写分离、加索引、缓存优化
等方式缓解,但效果有限,最终采用了分库分表来从根本上解决性能问题。

  • 业务介绍
    1,根据自己简历上的项目,想一个数据量较大业务(请求数多或业务累积大)
    2,达到了什么样的量级(单表1000万或超过20G)

  • 具体拆分策略
    1,水平分库,将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题
    2,水平分表,解决单表存储和性能的问题
    3,垂直分库,根据业务进行拆分,高并发下提高磁盘I0和网络连接数
    4,垂直分表,冷热数据分离,多表互不影响

分担了访问压力、解决存储压力

分库分表的时机:

前提:项目业务数据逐渐增多,业务发展比较迅速【单表数据量达1000W或20G以后】
② 优化解决不了性能问题(主从读写分离、查询索引)
IO瓶颈(磁盘IO、网络IO)、CPU瓶颈(聚合查询、连接数太多)

拆分策略【垂直 ≈ 微服务、水平 ≈ 分配数值】

  • ==垂直拆分==
    • 垂直分库:以表为依据,根据业务将不同表拆分到不同库中
      (特点:按业务对数据分级管理、维护、监控、扩展;在高并发下,提高磁盘IO和数据量连接数)
      • tb_user → 用户微服务
      • tb_order → 订单微服务
      • tb_sku → 商品微服务
    • 垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中
      (把不常用的字段单独放在一张表;把text, blob等大字段[描述]拆分出来放在附表中)
      (特点:冷热数据分离、减少IO过渡争抢,两表互不影响)
  • ==水平拆分==
    • 水平分库:将一个库的数据拆分到多个库中
      (解决了单库大数量,高并发的性能瓶颈问题;提高了系统的稳定性和可用性)
      路由规则
      • 根据id节点取模
      • 按id也就是范围路由,节点1(1-100万),节点2(100万-200万)
    • 水平分表:将一个库的数据拆分到多个表中(可以在同一个库内)
      (优化单一表数据量过大而产生的性能问题;避免IO争抢并减少锁表的几率)
分库后的问题:↓↓
  • 分布式事务一致性问题
  • 跨节点关联查询
  • 跨节点分页、排序函数
  • 主键避重
使用分库分表中间件
  • sharding-sphere
  • mycat

Spring框架中的单例bean是线程安全的吗?

不是线程安全的,是这样的

当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。

Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。
比如:我们通常在项目中使用的Springbean都是不可可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。

如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。

Spring框架中的bean是单例的

@Service
@Scope("singleton")
public class UserServiceImpl implements UserService{

}
  • singleton:bean在每个Spring IOC容器中只有一个实例
  • prototype:一个bean的定义可以有多个实例

Spring bean并没有可变的状态(比如Service类和DAO类), 所以在某种程度上说Spring的单例bean是线程安全的。但要尽可能的少创造可变参数比如count

@Controller
@RequeestMapping("/user")
public class UserController{
    private int count; //成员方法需要考虑线程安全问题

    @Autowired
    private UserService userService;

    @GetMapping("/getById/{id}")
    public User getById(@PathVariable("id") Integer id){
        count++;
        sout(count);
        return userService.getById(id);
    }
}
  1. 单例Bean就像共享单车
    • 整个小区(Spring容器)只有一辆共享单车(单例Bean),所有居民(线程)都要轮流骑这辆车。
    • 如果只是骑车(调用无状态方法),不会出问题。
    • 但如果有人在车筐里放东西(修改成员变量),下个人可能就会看到/改动这些东西。
  2. 什么时候安全?
    • 比如Service、DAO这类Bean,它们通常只干活不记账(没有成员变量),就像只提供骑行服务的单车,很安全。
    • 这也是为什么我们平时用@Autowired注入的Service不会出问题。
  3. 什么时候危险?
    • 如果Bean里有个计数器count(就像你代码里的例子),多个线程同时”+1”就会乱套。
    • 就像多个人同时往单车筐里放苹果,最后苹果数量肯定对不上。
  4. 怎么解决?
    • 方法一:不记账 → 永远不在Bean里放成员变量(推荐)
    • 方法二:用锁 → 像公共厕所那样,一个人用的时候锁门(加synchronized)
    • 方法三:每人发一辆车 → 改用@Scope(“prototype”),每次请求都新建Bean(但浪费资源)
  5. 实际开发建议
    • 大多数情况下,Service/Dao写成单例完全没问题
    • 遇到要记录状态的场景(比如计数器),要么改成prototype,要么把变量存在ThreadLocal里
    • 绝对不要在Controller里定义成员变量!你代码里的count就是个典型反例

简单说:单例Bean本身不是线程安全的,但只要我们遵守”不用成员变量记事情”的原则,就能安全使用。就像共享单车,只要大家都不往车筐里放私人物品,就不会有问题。

什么是AOP,你们项目中有没有用到AOP?

AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

常见AOP使用场景:
  • 拒绝策略记录操作日志

    nginx → 新增用户 → @Around(“pointcut()”) 环绕通知

  • 缓存处理

  • Spring中内置的事务处理

Spring中的事务是如何实现的
Spring支持 编程式事务管理声明式事务 管理两种方式

  • 编程式事务控制:需使用TransactionTemplate来进行实现,对业务代码有侵入性,项目中很少使用
  • 声明式事务管理:声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

AOP(面向切面编程)可以理解为 “在不修改原有代码的情况下,给程序动态添加功能”

生活中的例子:
  • 假设你开了一家咖啡店,主要业务是 做咖啡(核心业务)。
  • 但除了做咖啡,你还要 记录销售日志、检查权限、处理异常(如咖啡机坏了)等(横切关注点)。
  • 如果用传统OOP(面向对象编程),你需要在每个做咖啡的方法里都写日志、权限检查代码,这样代码会变得臃肿且难以维护。
  • 而AOP的做法是:把这些公共逻辑(如日志、权限)抽出来,做成一个“切面”,然后“织入”到需要的地方,不影响原有业务代码

AOP的核心概念
  1. 切面(Aspect):封装横切逻辑的模块(比如日志、事务)。
  2. 连接点(Join Point):程序执行的点(如方法调用、异常抛出)。
  3. 通知(Advice):切面在连接点执行的动作(如方法执行前、后、异常时做什么)。
  4. 切点(Pointcut):定义哪些连接点会被切面影响(如“所有Service层的方法”)。
  5. 织入(Weaving):把切面应用到目标对象的过程(编译期、类加载期、运行时)。

Spring中事务失效的场景有哪些?

异常捕获处理,自己处理了异常,没有抛出,解决:手动抛出
抛出检查异常,配置rollbackFor属性为Exception
非public方法导致的事务失效,改为public

考察对spring框架的深入理解、复杂业务的编码经验

  • ==异常捕获处理==【异常被try-catch吃掉】

    原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉【Spring 的事务是基于 AOP 的,只有方法抛出异常,事务管理器才能感知并触发回滚;你在方法内部 try-catch 了异常,但没有再往外抛,就会导致事务不能回滚

    解决在catch块添加throw new RuntimeException(“转账失败”) 抛出

  • ==抛出检查异常==

    原因:Spring 默认只对 非检查异常(RuntimeException 及其子类)、 进行回滚

    @Transactional
    public void update(...) throw FileNotFoundException{
        ...
        new FileInputStream("dddd")
        ...
    }
    

    解决:配置rollbackFor属性

    @Transcational(rollbackFor=Exception.class)
    
  • ==非public方法==
    Spring 的事务本质是基于 AOP 代理实现的,而 AOP 默认只对 public 方法生效。

    @Transcational(rollbackFor=Exception.class)
    void update(...) throw FileNotFoundException{
        ...
        new FileInputStream("dddd")
        ...
    }
    

    原因:Spring为方法创建代理、添加事务通知、前提条件都是该方法是public
    解决:把方法改为public

  • ==同类内部调用,导致代理失效==

    原因:Spring AOP 基于代理机制。如果类内部方法调用类内的另一个 @Transactional 方法,实际上不会经过代理,事务不会生效。

    // ❌ 会失效
    public void methodA() {
        methodB(); // 不经过代理
    }
      
    @Transactional
    public void methodB() {
        // 无效
    }
    
    • 解决
      • 将方法调用抽出到另一个 bean 中;
      • 或使用 AopContext.currentProxy() 获取当前代理对象执行调用。

Spring的bean的生命周期?

Spring容器是如何管理和创建bean实例
方便调试和解决问题

① 通过BeanDefinition获取bean的定义信息 [Spring 会将 XML 或注解配置的 Bean 信息封装成 BeanDefinition 对象,用于描述 Bean 的元数据信息,如 class 类型、作用域、是否懒加载等]
② 调用构造函数实例化bean [通过构造函数或工厂方法创建 Bean 对象,还没进行依赖注入]
③ bean的依赖注入 [Spring 根据 BeanDefinition 中的配置信息,进行依赖注入,例如通过 @Autowired@Resource 等注解注入其它 Bean]
④ 处理Aware接囗回调(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
⑤ Bean的后置处理器BeanPostProcessor-前置
⑥ 初始化方法(InitializingBean、init-method)
⑦ Bean的后置处理器BeanPostProcessor-后置
⑧ 销毁bean

BeanDefinition

Spring容器在进行实例化时,会将xml配置的< bean >的信息封装成一个BeanDefinition对象,Spring根据BeanDefinition来创建Bean对象,里面有很多的属性来描述Bean

<bean id="userDao" class="com.itheima.dao.impl.UserDaolmpl" lazy-init="true"/><bean id="userService" class="com.itheima.service.UserServicelmpl" scope="singleton">
  <property name="userDao" ref="userDao"></property>
</bean>

Spring中的循环引用?

循环依赖:循环依赖其实就是循环引用, 也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A
★ 循环依赖在spring中是允许存在,spring框架**依据三级缓存已经解决了大部分的循环依赖**
一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
二级缓存:缓存早期的bean对象(生命周期还没走完)
三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

✅ 处理流程简要说明:

  1. Spring 创建 A → A 依赖 B → 创建 B
  2. B 依赖 A,发现 A 还没完成创建
  3. Spring 把 A 的 半成品(early reference) 暴露到二级缓存,让 B 先注入用
  4. 最后 A 初始化完成,加入一级缓存

✅ 这样就解决了大多数基于 setter 注入 / 字段注入 的循环依赖问题。

构造方法出现了循环依赖怎么解决?

A依赖于B,B依赖于A,注入的方式是构造函数
原因:由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入
解决方案:使用**@Lazy进行懒加载**,什么时候需要对象再进行bean对象的创建

public A(@Lazy B b){
sout(“A的构造方法执行了”);
this.b=b;
}

@Component @Component
public class A{ → ← public class B{
@Autowired ↑ ↑ @Autowired
private B b; →↑ ↑← private A a;
} }

🧠 三大缓存:

缓存级别 对应源码字段名 作用说明
一级缓存 singletonObjects 已完全初始化的 Bean(生命周期完成),正式放入单例池
二级缓存 earlySingletonObjects 暂时暴露的半成品 Bean 实例(未执行初始化方法)
三级缓存 singletonFactories 存的是创建代理对象的 ObjectFactory,用于解决代理类循环依赖
Spring 把 A 的 半成品(early reference) 暴露到二级缓存,让 B 先注入用为啥要放到二级缓存 而不是一级呢?

✅ 答案核心:

因为此时 A 只是一个 尚未完成初始化的半成品对象,它还没有执行:

  • 属性填充(依赖注入)
  • Aware 接口回调
  • 初始化方法(如 @PostConstructafterPropertiesSet()

➡️ 这个 Bean 还不“完整”,不能放入正式的一级缓存!🔍 为什么不能直接放入一级缓存?

一级缓存是 singletonObjects,是 Spring 的“成品仓库”:

  • 放入这个缓存意味着:
    • Bean 已经完成生命周期(包括依赖注入、初始化)
    • 可以被别人安全引用

但在处理循环依赖时,我们是中途打断流程,把未初始化完成的 Bean 暴露出来给另一个 Bean 注入(比如 A 注入给 B)

🔴 如果此时放入一级缓存,有两个风险:

  1. 状态不一致风险
    其他 Bean 拿到这个未初始化完成的 Bean,会误以为它是“成品”,使用后可能报错或引发不一致。
  2. 生命周期混乱
    BeanPostProcessor、InitializingBean、@PostConstruct 等生命周期操作可能被跳过或错乱,严重破坏 Spring 的生命周期管理逻辑。

✅ 二级缓存的作用正是:**”暴露早期引用”,但不当成成品!**

// 二级缓存 earlySingletonObjects:仅仅作为“抢先使用”的通道
this.earlySingletonObjects.put(beanName, earlyReference);

它是 Spring 对“对象未完成但又必须提前使用”这个两难问题的妥协方案。✅ 最终完整的 Bean 放入一级缓存:

当整个 Bean 初始化完成(属性注入 + 初始化方法 + 后置处理器都执行完),才会放入一级缓存:

addSingleton(beanName, exposedObject); // 放入 singletonObjects


那按照你这样说 三级缓存又是干啥的?你问得非常到位!
✅ 我们已经知道:

  • 一级缓存:成品 Bean 的正式仓库
  • 二级缓存:未初始化完成的“早期引用”,暴露给依赖它的 Bean 使用。
✅ 简明结论(先记住):

三级缓存的本质作用是:为了解决“代理对象”在循环依赖中的提前暴露问题。

🎯 三级缓存的角色是什么?

三级缓存存的是:

Map<String, ObjectFactory<?>> singletonFactories

也就是说:它不是直接存 Bean 实例本身,而是存一个对象工厂 ObjectFactory,这个工厂可以在需要的时候返回一个 Bean(甚至是它的代理对象)。

🧠 为什么需要三级缓存?——解决AOP 代理对象的提前暴露

Spring 在三级缓存里放一个 ObjectFactory,用于 延迟构建代理对象

等到别的 Bean(比如 B)需要注入 A 时,先从二级缓存找不到,就用三级缓存的 ObjectFactory.getObject() 来构造这个 Bean,此时可以通过 SmartInstantiationAwareBeanPostProcessor(比如 AOP 后置处理器)提前生成代理。

什么是Spring的循环依赖??

==一级缓存==作用:限制bean在beanFactory中只存一份,即实现singleton scope,解决不了循环依赖

如果想打破循环依赖,就需要一个中间人的参与,这个中间人就是==二级缓存==如果一个对象是代理对象(被增强了)就不行

针对如果是代理对象的话如何解决呢? → ==三级缓存==

那如果构造方法出现了循环依赖怎么解决?

@Component @Component
public class A{ → ← public class B{
private B b; ↑ ↑ private A a;
public A(B c){ →↑ ↑← public B(A c){
sout(“A的构造方法执行了”) sout(“B的构造方法执行了”)
this.b=b; this.b=b;
} }
} }

报错信息:Is there an unresolvable circular reference?
解决:@Lazy 延迟加载→什么时候需要对象的时候什么时候实例化对象

public A(@Lazy B b){
 sout("A的构造方法执行了");
 this.b=b;
}

Spring解决循环依赖是通过三级缓存

// 单实例对象注册器
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private static final int SUPPRESSED EXCEPTIONS LIMIT= 100;  
private final Map<String, Object>singletonObjects = new ConcurrentHashMap(256); 一级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16); 三级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16); 二级缓存 
}
缓存名称 源码名称 作用
一级缓存 singletonObject 单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
二级缓存 earlySingletonObjects 缓存早期的bean对象(生命周期还没走完)
三级缓存 singletonFactories 缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

SpringMVC的执行流程知道吗?

  • 视图阶段(老旧JSP等)
  • 前后端分离阶段(接口开发,异步)

==视图阶段(jsp)==

  • 用户发送出请求到前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
  • HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有)
  • DispatcherServlet调用HandlerAdapter(处理器适配器)HandlerAdapter经过适配调用具体的处理器(Handler/Controller)Controller执行完成返回
  • ModelAndView对象HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet
  • DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)
  • ViewReslover解析后返回具体View(视图)
  • DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
  • DispatcherServlet响应用户

==前后端分离阶段(接口开发,异步请求)==

  • 用户发送出请求到前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
  • HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
  • DispatcherServlet调用HandlerAdapter(处理器适配器)
  • HandlerAdapter经过适配调用具体的处理器(Handler/Controller)

✅ 一、SpringMVC 是什么?

SpringMVC 是基于Servlet 的原生请求处理模型封装的一套 MVC Web 框架,它通过 DispatcherServlet 实现请求分发,解耦了控制器、视图解析器等组件之间的关系。

🚀 1. 用户发起请求

浏览器访问一个 URL,例如:http://localhost:8080/user/list,请求会先到达 前端控制器 DispatcherServlet


🔄 2. DispatcherServlet 接收到请求

DispatcherServlet 是 SpringMVC 的核心入口,用于请求分发和生命周期管理。


🔎 3. 查找 HandlerMapping(处理器映射器)

  • DispatcherServlet 调用 HandlerMapping 来查找当前请求所匹配的 Handler(Controller 方法)
  • 会封装成 HandlerExecutionChain,里面包含目标处理器和拦截器链。

⚙️ 4. 调用 HandlerAdapter(处理器适配器)

SpringMVC 不直接调用 Handler,而是交给 HandlerAdapter 来统一调用逻辑(比如支持 @RequestMapping/@RestController 方法)。


🧠 5. 执行 Handler(也就是 Controller 方法)

通过适配器调用具体的 Controller 中的业务处理方法,如:

java复制代码@GetMapping("/user/list")
public List<User> list() {
    return userService.findAll();
}

📦 6. 返回 ModelAndView(传统视图模式)或 @ResponseBody 数据(前后端分离)

  • 传统 MVC 场景下,Controller 返回一个 ModelAndView
  • 如果是前后端分离,通常会返回 JSON 数据,经过 HttpMessageConverter 处理后直接写入响应体。

🪞 7. 调用 ViewResolver(视图解析器)【视图模式专属】

  • 如果返回的是视图名(如 “userList”),SpringMVC 会调用 ViewResolver 解析为具体的 JSP 或 Thymeleaf 模板。

🎨 8. 渲染视图 View(视图模式专属)

  • 将模型数据(Model)填充进视图模板,生成 HTML 页面。

📤 9. DispatcherServlet 返回响应给浏览器

  • 前后端分离下是 JSON 响应;
  • 传统模式下是完整渲染后的 HTML。
  [用户请求]
       ↓
 DispatcherServlet
       ↓
 HandlerMapping → 找到 Handler + 拦截器链
       ↓
 HandlerAdapter  → 统一执行 Handler
       ↓
 Controller      → 执行业务逻辑
       ↓
 返回 ModelAndView / JSON
       ↓
(传统)ViewResolver → 找视图模板
       ↓
 渲染视图 / 写入 JSON 响应体
       ↓
 DispatcherServlet 响应浏览器

✅ 四、不同开发阶段下的区别

阶段 返回值 是否走视图解析器 常见注解
JSP 阶段 ModelAndView ✅ 是 @Controller
前后端分离 JSON 数据 ❌ 否 @RestController + @ResponseBody

🎯 五、总结金句(可背):

SpringMVC 核心就是一个请求经过 DispatcherServlet,根据 HandlerMapping 找到处理器,由 HandlerAdapter 调用 Controller 执行逻辑,最终通过视图解析或消息转换,返回结果给客户端。

SpringBoot自动配置原理?

@SpringBootApplication = 
    @SpringBootConfiguration +
    @EnableAutoConfiguration +
    @ComponentScan

SpringBoot中最高频的一道面试题,也是框架最核心的思想
==@SpringBootConfiguration==:该注解与 @Configuration 注解作用相同,用来声明当前也是一个配置类
==@EnableAutoConfiguration==:SpringBoot实现自动化配置的核心注解,通过配置选择器导入自动配置类
==@ComponentScan==:组件扫描,默认扫描当前引导类所在包及其子包

1,在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:

  • @SpringBootConfiquration
  • @EnableAutoConfiquration
  • @ComponentScan

2,其中@EnableAutoConfiguration是实现自动化配置的核心注解。该注解通过@Import注解导入对应的配置选择器。内部就是读取了该项目和该项目引用的jar包的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中

3,条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。

package com.itheima;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// SpringBoot的启动类
// 注意: 我们写的代码要在启动类的包或者子包中
// @SpringBootApplication注解中包含了 @ComponentScan,没有指定扫描哪个包,默认扫描当前类所在的包和子包
@SpringBootApplication
public class Day15TliasManagement01IocDiApplication {
    // 启动项目, 内嵌的Tomcat会启动, 把项目部署到这个内嵌Tomcat中
    public static void main(String[] args) {
        SpringApplication.run(Day15TliasManagement01IocDiApplication.class, args);
    }
}

按住ctrl+左键点击@SpringBootApplication会弹到SpringBootApplication.class界面

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)

按住ctrl+左键点击@EnableAutoConfiguration会弹到EnableAutoConfiguration.class界面

# @Import({AutoConfigurationImportSelector.class})
# AutoConfigurationImportSelector是自动配置的选择器 
# 会加载META-INF中的spring.factories文件的自动配置类...AutoConfiguration...
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

Spring框架常见的注解有哪些?

注解 说明
@Component、@Controller、@Service、@Repository 使用在类上用于实例化Bean
@Autowired 使用在字段上用于根据类型依赖注入
@Qualifier 结合@Autowired一起使用用于根据名称进行依赖注入
@Scope 标注Bean的作用范围默认单例的
@Configuration 指定当前类是一个Spring配置类,当创建容器时会从该类上加载注解
@ComponentScan 用于指定Spring在初始化容器时要扫描的包
@Bean 使用在方法上,标注将该方法的返回值存储到Spring容器中
@Import 使用@Import导入的类会被Spring加载到IOC容器中
@Aspect、@Before、@After、@Around、@Pointcut 用于切面编程(AOP)

SpringMVC框架常见的注解有哪些?

注解 说明
@RequestMapping 用于映射请求路径,可以定义在类上和方法上。用于类上,则标识类中的所有的方法都是以该地址作为父路径
@RequestBody 注解实现接收http请求的json数据,将json转换为java对象
@RequestParam 指定请求参数的名称
@PathViriable 从请求路径中获取请求参数(/user/{id}),传递给方法的形式参数
@ResponseBody 注解实现将Controller方法返回对象转换成json对象响应给客户端
@RequestHeader 获取指定的请求头数据
@RestController @Controller + @RequestBody

SpringBoot常见的注解有哪些?

注解 说明
@SpringBootConfiguration 组合了 -@Configuration注解,实现配置文件的功能
@EnableAutoConfiguration 打开自动配置的功能,也可以关闭某个自动配置的选项
@ComponentScan Spring组件扫描

MyBatis执行流程?

  • 读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
  • 构造会话工厂SqlSessionFactory
  • 会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
  • 操作数据库的接口,Executor执行器,同时负责查询缓存的维护
  • Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
  • 输入参数映射
  • 输出结果映射

执行流程从读取配置文件、创建 SqlSession、查找 MappedStatement、参数映射、执行 SQL 到结果映射,每个环节都有对应的组件协作完成。

  • 理解了各个组件的关系
  • Sql的执行过程(参数映射、sql解析、执行和结果处理)

MyBatis是否支持延迟加载?

  • 延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。
  • Mybatis支持一对一关联对象一对多关联集合对象的延迟加载
  • Mybatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true/false,默认是关闭的
<settings>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>

延迟加载(懒加载):查询主对象时不立即加载关联对象,而是在访问关联属性时再去执行查询语句加载数据
查询 User 时不查 Order,只有调用 user.getOrders() 时,才执行 select * from order where user_id = ?

延迟加载的底层原理知道吗?

MyBatis 延迟加载的核心是代理模式 + 拦截器机制,底层主要通过 CGLIB 创建目标对象的代理对象

  • 使用CGLIB创建目标对象的代理对象
  • 当调用目标方法时,进入拦截器invoke方法,发现目标方法是nul值,执行sql查询
  • 获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了

查询用户的时候,把用户所属的订单数据也查询出来,这个是==立即加载==
查询**用户的(sql)时候,暂时不查询订单数据,当需要订单的时候,再查询订单(sql)**,这个就是==延迟加载==

执行流程 👇

  1. 查询主对象时,不会立即查关联对象,而是用 CGLIB 创建一个代理对象(继承目标类)
  2. 当访问关联属性时,进入代理类的 intercept 方法(拦截器)
  3. 拦截器判断属性是否已加载
    • 没加载 → 执行对应的 SQL(如 selectOrdersForUser
    • 查出结果后 → 通过 setXxx() 方法注入属性值
  4. 属性值设置好之后,后续访问就是直接取值,不再触发数据库查询

延迟加载的实现步骤:

  1. 配置开启延迟加载: 在MyBatis的配置文件中(通常是mybatis-config.xml),需要设置两个属性:
    • lazyLoadingEnabled=true:开启延迟加载。
    • aggressiveLazyLoading=false:关闭积极的延迟加载,即访问对象的时候不会立即加载其所有属性。
  2. 映射文件配置: 在对应的Mapper映射文件中,对于需要延迟加载的关联查询,使用select标签定义延迟加载的SQL语句,并通过fetchType="lazy"属性明确指定使用延迟加载。
  3. 创建代理对象: 当执行查询操作时,MyBatis不会立即执行关联查询的SQL,而是返回一个代理对象。这个代理对象是使用CGLIB库创建的,它继承自目标对象。
  4. 拦截器方法调用: 当我们首次访问这个代理对象的某个方法(比如访问订单详情)时,实际上会调用CGLIB生成的代理对象的拦截器方法(intercept方法)。在拦截器方法中,会判断当前要访问的属性是否已经被加载:
    • 如果属性已经被加载,则直接返回属性值。
    • 如果属性未被加载,则会执行之前定义好的延迟加载SQL语句,从数据库中查询数据。
  5. 设置属性值: 查询得到数据后,MyBatis会将这些数据设置到目标对象的相应属性上,这样下次访问该属性时,就不需要再次查询数据库了。

底层原理:

  • CGLIB代理:MyBatis使用CGLIB库创建目标对象的代理,当调用目标方法时,实际上会进入拦截器(Interceptor)的intercept方法。
  • 拦截器逻辑:在拦截器中,会判断当前调用的方法是否需要触发延迟加载。如果需要,则执行延迟加载的SQL查询。
  • 结果处理:查询结果会被处理并设置到目标对象的属性上,这样目标对象的相关属性就持有了数据,后续访问将直接返回这些数据,而无需再次查询。MyBatis在执行完延迟加载的SQL查询后,会获取查询结果,并将这些结果映射到目标对象的相应属性中

示例说明:

假设有一个用户User和订单Order的关系,在查询用户时,通常不会立即加载其订单信息,而是当需要时再加载。以下是简化的代码示例:

<!-- UserMapper.xml -->
<resultMap id="userMap" type="User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <!-- 延迟加载订单信息 -->
  <collection property="orders" column="id" ofType="Order" select="selectOrdersForUser" fetchType="lazy"/>
</resultMap>

<select id="selectUser" resultMap="userMap">
  SELECT * FROM user WHERE id = #{id}
</select>

<select id="selectOrdersForUser" resultType="Order">
  SELECT * FROM order WHERE user_id = #{id}
</select>

在上述配置中,当调用selectUser查询用户信息时,不会立即查询订单信息。只有当程序中访问User对象的orders属性时,才会执行selectOrdersForUser查询,这就是延迟加载的具体实现。

MyBatis的一级、二级缓存用过吗?

  • 一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
  • 二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用PerpetualCache,HashMap 存储。需要单独开启,一个是核心配置,一个是mapper映射文件

MyBatis的二级缓存什么时候会清理缓存中的数据?

  • 当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear。
  • 本地缓存,基于PerpetualCache,本质是一个HashMap
  • 一级缓存:作用域是session级别
    • 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
  • 二级缓存:作用域是namespace和mapper的作用域,不依赖于session
    • 二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用 PerpetualCache
      HashMap 存储

注意事项
  • 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear
  • 二级缓存需要缓存的数据实现Serializable接口
  • 只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中

SpringCloud篇

SpringCloud 5大组件有哪些?

回答原则:简单的问题不能答错

通常情况 SpringCloudAlibba
Eureka:注册中心 Nacos:注册中心/配置中心
Ribbon:负载均衡 Ribbon:负载均衡
Feign:远程调用 Feign:远程调用
Hystrix:服务熔断 sentinel:服务保护
Zuul/Gateway:网关 Gateway:服务网关

服务注册和发现是什么意思? SpringCloud 如何实现服务注册发现?

  • 我们当时项目采用的eureka作为注册中心,这个也是SpringCloud体系的一个核心组件
  • 服务注册:服务提供者需要把自己的信息注册到eureka来保存这些信息,比如**服务名称、ip、端口**等等
  • 服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者利用负载均衡算法,选择一个发起调用
  • 服务监控:服务提供者会每隔30秒向eureka发送心跳,报告健康状态,如果eureka服务90秒没有收到心跳,从eureka中剔除
  • 微服务中必须要使用的组件,考虑我们使用微服务的程度
  • 注册中心的核心作用是:服务注册和发现
  • 常见的注册中心:eureka、nocas、zookeeper

请你说一下nacos与eureka的区别?

  • Nacos与Eureka的共同点 (注册中心)
    • 都支持服务注册和服务拉取
    • 都支持服务者心跳方式做健康检测
  • Nacos与Eureka的区别 (注册中心)
    • Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
    • 临时实例心跳不正常会被剔除,非临时实例则不会被提出
    • Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
    • Nacos集群默认采用AP方式高可用模式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
  • Nacos还支持了配置中心,Eureka只有注册中心,也是选择选用nacos的一个重要原因

把RestTemplate替换成OpenFeign后它们的底层还是一样的吗?OpenFeign是远程调用

OpenFeign的底层原理也是根据服务名称,首先去远程注册中心拉取服务列表,底层也会在本地缓存一份,也会根据负载均衡选出一个实例,又运用了jdk的动态代理生成代理类,也会涉及到反射机制,最终拼出完整的url,发起http远程调用

@FeignClient(name = "service-provider")
public interface ServiceProviderClient {
    // 定义接口方法,映射到服务提供者的具体API
    @GetMapping("/api/resource")
    String getResource();
}

✅ 一、使用方式不同

  • RestTemplate 是显式调用(自己写 URL,拼参数)

    restTemplate.getForObject("http://user-service/user/1", String.class);
    
  • OpenFeign 是声明式调用(只写接口 + 注解,SpringBoot自动帮你拼URL发请求)

    @FeignClient("user-service")
    public interface UserClient {
        @GetMapping("/user/{id}")
        String getUser(@PathVariable("id") Long id);
    }
    

✅ 二、底层原理差不多,但实现机制不同

对比点 RestTemplate OpenFeign
发起方式 手动构造 URL + 参数 注解接口 + 动态代理自动拼 URL
底层通信方式 HttpClient / OKHttp / JDK Http 同样也是基于 HttpClient 或 OKHttp
注册中心拉取服务 可搭配 Ribbon 手动实现服务发现 默认集成 Spring Cloud LoadBalancer 自动发现服务
负载均衡 手动配置 Ribbon 或 LoadBalancer 自动内置 LoadBalancer,基于服务名均衡选择
动态代理 有,基于 JDK 动态代理生成接口实现类
扩展能力 灵活性高,配置复杂 扩展性强,统一规范,支持熔断/重试/拦截器等

✅ 三、OpenFeign 更高级,集成更好

OpenFeign 是对 RestTemplate 的一层封装 + 声明式远程调用:

  • 内置了服务注册发现(Eureka/Nacos)
  • 内置了负载均衡(Ribbon / Spring Cloud LoadBalancer)
  • 可配合 Hystrix / Sentinel 实现熔断降级
  • 支持拦截器、日志、重试、压缩等功能
  • 统一异常处理、超时配置更方便

✅ 四、结论一句话总结:

虽然最终底层都是通过 HTTP 客户端发起请求(如 OkHttp / HttpClient),但 OpenFeign 是基于动态代理+注解的声明式封装,实现了更强大的远程调用能力和集成能力,远比 RestTemplate 更高级、更易维护。

如果面试官继续追问:“你们项目是怎么替换的?”,你可以说:

我们之前用 RestTemplate 是在业务代码里拼 URL,很冗余。后来统一封装为 OpenFeign,只保留接口定义,调用方更清晰,服务注册与发现、负载均衡也变成自动处理,配合 Sentinel 做了服务熔断与限流,提升了整体的系统健壮性。

怎么个自动处理法?

默认情况下 —— OpenFeign 自动处理

✅ 默认配置时:

  • 服务注册与发现:依赖 Nacos / Eureka 等注册中心,OpenFeign 会根据服务名自动从注册中心拉取可用实例。
  • 负载均衡:默认通过 Spring Cloud LoadBalancer(以前是 Ribbon)对服务列表进行轮询或权重等策略选择一个实例。
  • HTTP请求:通过 HttpClient / OkHttp 等客户端执行。

你只写:

@FeignClient("user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User getUser(@PathVariable("id") Long id);
}

OpenFeign 会自动做:

  • 拉取 user-service 的服务列表
  • 选出一个实例(负载均衡)
  • 拼接 URL 发起 HTTP 请求

🧠 一、什么叫“选出一个实例”?

微服务架构 中,一个服务往往会 部署多个实例 来应对高并发或容灾,例如:

服务名 实例地址
user-service 10.0.0.1:8080
user-service 10.0.0.2:8080
user-service 10.0.0.3:8080

当你通过 OpenFeign 发送请求:

@FeignClient("user-service")
User getUser(...);

你只写了一个服务名 user-service,但后台其实有多个实例,必须从这些实例中选出一个具体地址来发请求,比如选中 10.0.0.2:8080


⚖️ 二、这就是负载均衡要干的事!

✅ 负载均衡做的事:

就是 从多个可用实例中选择一个

✅ 为什么要选?不选行不行?

如果不做选择,就不知道到底该请求哪个服务器,容易:

  • 总是访问同一个实例 → 某一个实例过载
  • 有的实例空闲,有的压力大 → 资源利用不均衡
  • 某些实例宕机,没人检测 → 请求失败

✅ 所以负载均衡的作用是:

  • 分摊压力:让不同请求打到不同实例,防止某一个服务器崩掉
  • 提高可用性:某个实例挂了,下一次选择其他实例,系统还能继续用
  • 自动切换:负载均衡组件还能感知服务状态变化,动态更新可用实例列表

🧮 三、OpenFeign 背后使用了什么做负载均衡?

在 Spring Cloud 中,OpenFeign 默认集成了:

✅ Spring Cloud LoadBalancer(或老版 Ribbon)

它的核心就是在每次请求前调用:

choose("user-service")

然后根据你配置的策略,比如:

  • 轮询(RoundRobin)👉 按顺序一个一个来
  • 随机(Random)👉 随机挑一个
  • 最少连接数(LeastConnections)
  • 权重(Weighted)

最终得到一个实例,例如 10.0.0.2:8080,然后拼接成最终 URL:

http://10.0.0.2:8080/api/user/1

再发起 HTTP 请求。


🎯 总结一句话:

OpenFeign 中的“选出一个实例”就是 从多个服务实例中选择一个合适的来发送请求的过程,这个选择过程就是“负载均衡”负责的。它的目标是为了 提升性能、避免单点压力、实现高可用

你们项目负载均衡如何实现的?图1.1

微服务的负载均衡主要使用了一个组件Ribbon,比如,我们再使用feign远程调用的过程中,底层的负载均衡就是使用了Ribbon 【与RestTemplate不同,OpenFeign默认是LoadBalancer

  • 负载均衡Ribbon,发起远程调用feign就会使用Ribbon
  • Ribbon负载均衡策略有哪些?
  • 如果想自定义负载均衡策略如何实现?
Ribbon已经进入维护模式,Netflix不再积极开发新功能。而Spring Cloud LoadBalancer作为替代,不仅提供了Ribbon的核心功能,还引入了一些新特性和改进

Ribbon负载均衡策略有哪些?简单、权重、随机、区域

  • RoundRobinRule简单轮询服务列表来选择服务器
  • WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小
  • RandomRule随机选择一个可用的服务器
  • BestAvaliableRule:忽略那些短路的服务器,并选择并发数较低的服务器
  • RetryRule:重试机制的选择逻辑
  • AvaliabilityFilteringRule:可用性敏感策略,先过滤非健康的,再选择连接数较小的实例
  • ZoneAvoidanceRule:以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可用理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询
✅ Ribbon 常见负载均衡策略一览
策略名 简介 核心逻辑 适用场景
RoundRobinRule 轮询策略 依次选择服务列表中的每个实例,循环使用 简单、适用于服务性能相当、请求量均匀的场景
RandomRule 随机策略 随机选一个可用实例 测试环境、低并发系统或对分布无要求的场景
WeightedResponseTimeRule 权重 + 响应时间 根据服务实例响应时间动态调整权重,响应快的被选中几率高 适用于实例性能差异明显,希望高性能实例被优先选中
RetryRule 带重试机制的轮询 每次选择失败后会在一段时间内重试其他实例(默认使用 RoundRobinRule) 适用于请求容错性强、临时性网络波动频繁的情况
BestAvailableRule 最少并发策略 忽略短路(熔断)的实例,选择并发数最少的可用实例 适用于高并发下控制服务压力
AvailabilityFilteringRule 可用性过滤策略 过滤掉连接失败次数多的和并发高的实例,避免访问不健康服务 适用于系统对可用性要求高、网络波动大的场景
ZoneAvoidanceRule(默认) 区域感知策略 综合评估 zone(区域)内实例的可用性和负载(Zone可以理解为机房或机架),选出最优 zone 再轮询选择服务 适用于跨机房、跨地域部署,希望优先选择本地/低延迟实例的系统

📌 补充小Tips

  • Spring Cloud 2020 后 Ribbon 官方不再维护,推荐用 Spring Cloud LoadBalancer 替代,但思想类似。
  • 如果你项目还用 Ribbon,推荐配合 Hystrix 或 Sentinel 做服务熔断和限流

如果想自定义负载均衡策略如何实现?图1.2

  • 创建类实现IRule接口,可以指定负载均衡策略(全局)
  • 在客户端的配置文件中,可以配置某一个服务调用的负载均衡(局部)

首先,你需要创建一个类来实现 IRule 接口,这样就能自定义负载均衡的策略。
实现 IRule 接口

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAwareLoadBalancer;
import com.netflix.loadbalancer.RandomRule;

import java.util.List;

public class CustomLoadBalancerRule implements IRule {

private IRule delegate = new RandomRule();  // 默认策略

@Override
public Server choose(Object key) {
  // 在这里实现自己的负载均衡算法
  // 比如,你可以使用 RoundRobin、Random 或者基于健康检查的策略
  return delegate.choose(key);
}

@Override
public void setLoadBalancer(ZoneAwareLoadBalancer<?> lb) {
  delegate.setLoadBalancer(lb);
}

@Override
public ZoneAwareLoadBalancer<?> getLoadBalancer() {
  return delegate.getLoadBalancer();
}
}

然后,你需要在 Spring 配置类或者启动类上注入该自定义的负载均衡策略。
配置 Bean 注册到容器(全局策略)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfig {

@Bean
public IRule customLoadBalancerRule() {
  return new CustomLoadBalancerRule();
}
}

SpringCloud中什么是服务雪崩,怎么解决这个问题?

  • 什么是==服务雪崩==?一个服务失败,导致整条链路的服务都失败的情形
  • 服务雪崩:当某个服务因为故障不可用,导致依赖它的上游服务纷纷失败,进而整个系统连锁崩溃的现象
    • 常发生在服务调用链路较长
    • 一个服务挂了,所有调用它的服务都会超时挂起,线程资源被耗尽,最终整个系统瘫痪

📌 通俗比喻:你访问淘宝商品详情页面 → 商品服务依赖库存服务 → 库存服务挂了 → 商品服务一直卡着等 → 网页加载失败 → 淘宝崩了

  • ==熔断降级== && ==服务熔断==(解决)Hystix 服务熔断降级

服务降级部分服务不可用:服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与Feign接口整合,编写降级逻辑 (某个服务挂了或响应慢,不让请求卡住,而是返回预设的“备胎数据”,用户体验不会很差)

服务熔断整个服务不可用:默认关闭,需要手动打开,如果监测到10秒内请求的失败率超过50%,就触发熔断机制。之后每隔5秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求

  • 限流(预防)微服务限流(漏桶算法、令牌桶算法)

    限制单位时间的请求数,避免流量洪峰打垮服务

    • 漏桶算法:匀速出水,适合平滑限流
    • 令牌桶算法:按需取令牌,适合突发流量控制

    📍 实现工具:Sentinel、Gateway限流、Bucket4j、RateLimiter

你们项目中有没有做到限流?怎么做的?&& 限流常见的算法有哪些??

① 先来介绍一下业务,什么情况下去做限流,需要说明QPS具体是多少

  • 我们有一个活动,到了假期就会抢购优惠券,QPS最高可以达到2000,平时10-50之间,为了应对突发流量,需要做限流
  • 常规限流,为了防止恶意攻击,保护系统正常运行,我们当时系统能够承受最大的QPS是多少(压测结果)

nginx限流

  • 控制速率(突发流量),使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量
  • 控制并发数,限制单个ip的连接数和并发链接的总数

网关限流

  • 在SpringCloudGateway中支持局部过滤器RequestRateLimiter来做限流,使用的是令牌桶算法
  • 可以根据ip或路径进行限流,可以设置每秒填充平均速率,和令牌桶总容量

解释原理:

QPS(Queries Per Second,每秒查询率)是衡量一个系统处理请求能力的指标,它表示服务器在一秒钟内能够处理的查询数量。这个指标常用于数据库和web服务器等应用,以评估系统在高并发情况下的性能。
以下是对您提到的两句话的分析:

  1. 活动期间的高并发处理:
  • 背景知识: 在电子商务等应用中,促销活动往往会引起用户的大量点击和购买行为,导致短时间内流量剧增。
  • 限流原理: 为了应对这种突发的高流量,系统需要实施限流措施。限流是为了保护系统资源不被过度消耗,确保系统的稳定性和可靠性。常见的限流算法有固定窗口、滑动窗口、令牌桶和漏桶等。

固定窗口: 假设每 1 分钟允许 100 次请求,10:00:00 到 10:01:00 期间的 100 次请求被允许,超出 100 次则会被限流,10:01:00 到 10:02:00 则重新开始计算。

滑动窗口: 每 60 秒内最多允许 100 次请求,滑动窗口的时间长度为 60 秒,窗口内的请求数会随着时间滑动更新,防止请求在时间边界上积压

令牌桶:假设每秒生成 10 个令牌,令牌桶的容量为 100 个令牌。如果 1 秒内有 15 个请求到达,则前 10 个请求能获得令牌并继续执行,剩余的 5 个请求需要等到下一个时间窗口令牌生成后再执行。

漏桶:假设每秒钟流出 10 个请求,漏桶的容量为 100 个请求。如果 1 秒钟内接收了 30 个请求,系统只会处理 10 个请求,剩余的 20 个请求被丢弃,直到下一个时间点。

  • 实施方式: 在您提到的情况下,可以采用以下策略:
    • 预判性扩容: 根据历史数据和活动规模预测流量,提前进行服务器资源的扩容。
    • 动态限流: 在活动期间,根据实时监控的QPS数据动态调整限流阈值,保证系统平稳运行。
    • 排队处理: 对于超出系统处理能力的请求,可以采用队列进行缓冲,分批次处理。
  1. 常规限流与系统最大承受QPS:
  • 背景知识: 常规限流是为了在日常运行中防止恶意攻击(如DDoS攻击)和保护系统资源不被滥用。
  • 压测结果: 系统的最大承受QPS是通过压力测试得出的。压力测试(也称为负载测试)是通过模拟高并发访问来测试系统的极限性能,以确定系统在保证稳定运行的前提下能够承受的最大QPS。
  • 原理分析:
    • 保护系统: 通过设定一个QPS上限,可以防止系统过载,保障系统的正常运行。
    • 资源分配: 了解系统的最大承受QPS有助于合理分配资源,如数据库连接、内存和CPU等。
    • 用户体验: 适当的限流可以保证用户的体验,避免因系统过载导致的响应缓慢或服务不可用。
      在实施限流策略时,还需要考虑以下因素:
  • 业务优先级: 对于不同的业务请求,可能需要有不同的限流策略,优先保证核心功能的可用性。
  • 用户体验: 限流策略应尽量减少对用户体验的影响,例如通过友好的错误提示或降级方案。
  • 数据监控: 实时监控系统的QPS和其他关键指标,以便快速响应并调整限流策略。
    综上所述,限流是确保系统在高并发情况下稳定运行的重要措施,而了解系统的最大承受QPS是制定合理限流策略的基础。

为什么要限流?

  • 并发业务量大(突发流量)
  • 防止用户恶意刷接口
限流的实现方式:
  • ==Tomcat==单体项目可以,分布式不行:可以设置最大连接数 <Connector port="8080"...maxThreads="150"...>

  • ==Nginx==:漏桶算法固定速率露出(平滑)

    控制速率(突发流量)

  • ==网关==:令牌桶算法

  • 自定义拦截器

echos-gateway真实案例

🔷 1. 网关服务名称 + 动态发现配置

spring:
  application:
    name: ech-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.188.120:8848
  • 知识点:Gateway 是 Spring Cloud 架构中的 API 网关,用于请求路由、统一鉴权、日志跟踪、限流、熔断等功能。
  • 动态路由注册中心配置:接入 Nacos 注册中心,自动发现服务实例。

🔷 2. Gateway 路由规则(重点)

spring:
  cloud:
    gateway:
      routes:
        - id: zk-sam-service
          uri: lb://ech-sam-cs
          predicates:
            - Path=/iclock/**

🧠 面试要点:

  • id: 路由唯一标识
  • uri: lb://xxx: 表示使用 负载均衡方式 路由到注册中心中 xxx 的实例(通过 Ribbon/LoadBalancer 实现)
  • Path: 路径断言,匹配路径开头为 /iclock/** 的请求
  • filters: StripPrefix=1: 去除路径中的第一级前缀(如 /ech-service/a/b/a/b

🔷 3. 自动路由发现开启

discovery:
  locator:
    enabled: true
    lower-case-service-id: true

🧠 面试延申:

  • enabled=true:允许网关根据注册中心中注册的服务自动创建路由(简化配置)
  • lower-case-service-id=true:将服务名小写化,避免大小写不一致导致路由失败

面试官可能问:
“你们项目中是手动配置路由还是用 locator 自动发现?哪种方式更推荐?”

回答思路:

  • 自动发现适合内部测试环境,快速接入新服务
  • 生产建议手动配置,便于管理、加权限、加限流、避免误暴露

✅ 三、Feign配置部分(Gateway下游调用)

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
  httpclient:
    enabled: false
  okhttp:
    enabled: false

✅ 面试可延申:

  • Feign 的连接/读取超时时间如何设置?
  • loggerLevel 有哪些级别(NONE/BASIC/HEADERS/FULL)?
  • Feign 支持哪几种底层 HTTP 客户端?为什么要关掉 HttpClient/OkHttp?

✅ 四、Redis连接池配置(网关限流、缓存常用)

spring:
  redis:
    database: 12
    host: 192.168.188.120
    port: 6379
    lettuce:
      pool:
        max-active: 1000
        max-idle: 10
        min-idle: 5

✅ 面试展开:

  • 为什么使用 Redis?

    作为 限流、缓存、黑名单过滤、Token 存储 后端,具备高性能分布式能力

  • 你们用 Lettuce 还是 Jedis?区别在哪?


✅ 五、Actuator配置(服务监控)

management:
  server:
    port: 9090
  • 暴露 Spring Boot Actuator 指标信息,常用于结合 Prometheus/Grafana 做监控
  • 可暴露如 /actuator/health/metrics/gateway/routes

你们的微服务是怎么监控的?

我们项目中采用的skywalking进行监控的

  • skywalking主要可以监控接口、服务、物理实例的一些状态。特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,我们可以针对性的分析和优化。
  • 我们还在skywalking设置了告警规则,特别是在项目上线以后,如果报错,我们分别设置了可以给相关负责人发短信和发邮件,第一时间知道项目的bug情况,第一时间修复
skywalking

一个分布式系统的应用程序性能监控工具(Application Performance Management), 提供了完善的链路追踪能力,apache的顶级项目(前华为产品经理吴晟主导开源)

✅ 面试追问题 & 答法建议

面试官可能追问的问题 答题建议
🔸SkyWalking 是怎么接入项目的? 我们使用 Java Agent 的方式,启动时通过 -javaagent:/path/to/skywalking-agent.jar 加载探针,同时在 agent.config 中配置服务名、采集后端等
🔸和 Zipkin、Prometheus 相比有什么优劣? SkyWalking 支持 UI 更强,适合链路分析;Prometheus 更适合指标监控配合 Grafana;Zipkin 更轻量但功能少。SkyWalking 是综合性最强的一款
🔸SkyWalking 的数据存储用的什么? 默认是 ElasticSearch,也支持 H2(测试环境),生产建议搭配 ES 做查询与聚合
🔸你怎么通过 SkyWalking 优化过接口? 我们发现某个接口 RT 超过 1 秒,通过 Trace 发现是 MySQL 多表 Join 查询导致,优化了 SQL 才解决的
🔸SkyWalking 能采集哪些组件的数据? HTTP请求、Dubbo、MySQL、Redis、Kafka、RocketMQ、Elasticsearch 等主流中间件都有探针支持

✅ 加分拓展:如何实现全链路追踪?

SkyWalking 通过在每个服务部署时挂载探针(agent),实现对请求头中 traceId 的自动传递,采集:

  • 入参出参(拦截 Controller 层、Feign、RestTemplate)
  • RPC调用(如Dubbo/Feign)
  • 数据库执行耗时
  • Redis访问
  • MQ消息链路

最终统一聚合在 SkyWalking OAP 端,再通过 Web UI 展示,形成完整的 “调用链 + 指标 + 日志” 三位一体监控体系。

✅ 项目答题串联建议

你可以在项目介绍环节自然引入这段内容,例如👇:

为了保障我们微服务系统的稳定性,我们在项目中接入了 SkyWalking 作为 APM 工具。通过它我们做到了链路追踪、性能分析、异常预警等。特别在压测和上线之后,能第一时间通过短信和邮件告警通知我们,提升了系统稳定性和定位效率。

探针(Agent)本质上是一个Java 程序运行时的字节码增强器,可以在不修改源码的前提下,对目标应用的类和方法进行增强,从而实现请求数据的“埋点采集”。


✅ 简单理解:探针干了啥?

可以这样比喻👇

你写的业务代码是演员
探针就是藏在台下的摄像机
它在你表演的时候偷偷把你的一举一动都记录下来(记录你调了哪个接口,用了多久,是不是出错了)


✅ 具体工作原理:

  1. 运行时注入字节码
    • 探针是以 -javaagent 参数的形式加载到 JVM 中
    • JVM 启动时,探针会监听所有类的加载过程,选择特定的类(如 ControllerRestTemplateJdbcTemplateRedisTemplate)进行增强
  2. 插入监控逻辑
    • 在方法的前后插入监控逻辑:
      • 方法开始时记录时间
      • 方法执行完记录耗时、返回值
      • 如果抛出异常,也能捕获异常堆栈
  3. traceId 传递
    • 每个请求入口(比如 SpringMVC Controller)会生成一个全局 traceId
    • 这个 ID 会自动透传到下游服务,比如 Feign 调用、MQ 发送、数据库访问等
    • 最终串成一条完整的调用链路

✅ 示例:

比如你访问这个接口:

@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
    return userService.getById(id);
}

探针实际会在你这个方法前后偷偷插入逻辑(伪代码):

// 前置逻辑:记录 traceId、时间戳
recordTrace("traceId-xxx");
startTime = System.currentTimeMillis();

User result = userService.getById(id);

// 后置逻辑:记录耗时
long cost = System.currentTimeMillis() - startTime;
sendToSkywalking(traceId, methodName, cost);

探针就是自动化“打点采集 + 数据上传”的代码增强器,开箱即用,不入侵业务代码。

解释一下CAP和BASE分布式系统理论

  • CAP 定理(一致性、可用性、分区容错性)
  1. 分布式系统节点通过网络连接,一定会出现分区问题(P)
  2. 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足
  • BASE理论
  1. 基本可用
  2. 软状态
  3. 最终一致
  • 解决分布式事务的思想和模型
  1. 最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据(AP)
  2. 强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚(CP)
  • 分布式事务方案的指导
  • 分布式系统设计方向
  • 根据业务指导使用正确的技术选择
==CAP定理==分布式系统无法同时满足三个指标

CAP 定理是 数学证明出来的分布式理论限制不是人为规定的规则,而是无法回避的“物理规律”

  • ==Consistency==(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。主从一致
  • ==Availability==(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
  • ==Partition tolerance==(分区容错性):当出现网络分区现象后,系统能够继续运行
    • Partition(分区):因为网络故障或其他原因导致分布式系统中的部分节点与其他节点失去链接,形成独立分区
    • Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

结论:

  • 分布式系统节点之间肯定是需要网络链接的,分区 (P) 必然存在
  • 如果保证访问的高可用性(A)可以持续对外提供服务,但不能保证数据的强一致性 AP
  • 如果保证访问的数据强一致性(C)就要放弃高可用性 CP

🎯 CAP 的结论:

在分布式系统中,由于网络问题不可避免(P必选),所以只能在 C 和 A 之间选其一:

类型 特点 场景
CP 系统 放弃可用性,保证一致性 银行/支付系统(宁可服务不可用,也不能出错)
AP 系统 放弃一致性,保证可用性 电商商品浏览、社交系统(稍微不一致无所谓)

为什么 CAP 无法同时满足?

我们来看一个例子理解「一致性(C) vs 可用性(A) 在分区故障(P)下的矛盾」:


❗ 场景设定:

  • 系统有两个节点:节点A 和 节点B
  • 正常时,A 和 B 通过网络通信同步数据
  • 现在发生了网络分区(P):A 和 B 之间断网了!

🧩 你怎么选?C 和 A 只能二选一:

✅ 如果你要保证【一致性 C】:

  • 当客户端向 A 节点写数据时,为了保证一致性,A 必须等待 B 同步成功
  • 但现在 A 和 B 网络断了,同步不了
  • 所以 A 只能拒绝请求:不响应 —— ❌ 违反了可用性 A

✅ 如果你要保证【可用性 A】:

  • A 收到写请求就立刻接受并返回成功
  • 但 B 此时拿到的还是老数据
  • 数据就不一致了 —— ❌ 违反了一致性 C

🎯 所以:在【网络分区 P】已发生的前提下 ——

你只能选:C+A ❌,C+P ✅,A+P ✅

这就是 CAP 定理的本质逻辑。
所以我们在设计系统时必须做权衡 —— 这正是分布式架构的难点和美感所在。

==BASE理论==

BASE理论是对CAP的一种解决思路,包含三个思想:

  • ==Basically Avaliable==(基本可用):分布式系统在出现故时,允许损失部分可用性,即保证核心可用
  • ==Soft State==(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
  • ==Eventually Consistent==(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致性

🎯 举个通俗易懂的例子:

你去银行转账,系统提示:“资金将在2小时内到账”。这就是:

  • ✅ 系统可用(你能转账)
  • ✅ 软状态(中间状态是“处理中”)
  • ✅ 最终一致(2小时内一定到账)

这就符合 BASE 理论。


✅ BASE 与 CAP 的关系?

理论 类型 强调
CAP 理论限制 三选二原则,强调不可能同时满足一致性、可用性、分区容错
BASE 实践理念 放弃强一致性,追求最终一致,以换取系统可用性与性能
CAP如何选择?
  • CP[支付宝]或者AP[超级跑跑系统维护]
  • 在什么场合,可用性高于一致性?
    • 网页必须要保障可用性(一定能看到最重要 是不是最新的不重要)和分区容错
    • 支付的时候一定要保障一致性(我可以保证不可用 但我不允许余额不一致)和分区容错
  • 合适的才是最好的

你们采用哪种分布式事务解决方案?

● 简历上写的微服务,只要是发生了多个服务之间的写操作,都需要进行分布式事务控制

● 描述项目中采用的哪种方案(seataMQ)
⚪ seata的XA模式,CP,需要互相等待各个分支事务提交,可以保证强一致性,性能差 (银行业务 )
⚪ seata的AT模式,AP,底层使用undolog 实现,性能好 (互联网业务 )
⚪ seata的TCC模式,AP,性能较好,不过需要人工编码实现 (银行业务 )
⚪ MQ模式实现分布式事务,在A服务写数据的时候,需要在同一个事务内发送消息到另外一个事务异步,性能最好 (互联网业务 )

✅ 你的理解:Seata 的 XA ≈ CP,AT ≈ AP?

Seata 是实现,CAP 是原则

你这样理解有一定道理,但这两者并不是等价关系。
Seata 是一种“解决分布式事务问题的技术方案”,目的是确保多个数据库操作的一致性,属于 CAP 中的一致性实现策略之一,但不能反过来说它就是 CAP 的实现。

对比 Seata XA / AT 模式 CAP 理论中的 CP / AP
本质 一种分布式事务协议实现 一种分布式系统权衡模型
关注点 一致性、事务原子性 可用性 vs 一致性 vs 分区容忍性
使用背景 数据库操作级别的事务控制 整个分布式系统的架构设计选择

🟢 Seata 为什么要设计成 XA、AT 两种模式?

因为不同的业务场景对性能 vs 一致性 的需求不同,Seata 提供了“可插拔”式的事务解决方案:

模式 一致性 性能 特点
XA模式(两阶段提交) 强一致性 性能差、资源占用多 接近传统分布式事务,事务期间资源锁定
AT模式(自动补偿) 最终一致性 性能好 非侵入,靠 Undo Log 回滚操作,适用于大部分业务场景
  • Seata框架(XA、AT、TCC)
  • MQ

Seata架构

  • TC(Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,协调全局事务提交或回滚
  • TM(Transaction Manager) - 事务管理器:定义全局事务的范围、开启全局事务、提交或回滚全局事务
  • RM(Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚

Seata 的 XA 和 AT 确实在一致性与可用性方面体现了 CAP 的设计权衡,但它并不是 CAP 的实现,而是面向分布式事务的一种可插拔事务解决方案。Seata 架构之所以独立,是因为它提供了统一的事务协调服务,支持多种协议(XA/AT/TCC/SAGA),以满足不同业务场景对一致性和性能的需求,这在真实的微服务架构中非常关键。

分布式服务的接口幂等性如何设计?

  • 幕等:多次调用方法或者接口不务状态,可以保证重复调用的结果和单次调用的结果一致;常用于支付、下单等关键业务防止重复提交
  • 如果是**新增数据**,可以使用数据库的唯一索引
  • 如果是**新增或修改数据**
    • 分布式锁,性能较低
    • 使用token+redis来实现,性能较好
      ● 第一次请求,生成一个唯一token存入redis,返回给前端
      ● 第二次请求,业务处理,携带之前的token,到redis进行验证,如果存在,可以执行业务,删除token; 如果不存在,则直接返回,不处理业务

🧠 一、什么是幂等性?

  • 幂等性是指:一次和多次请求同一个接口,对资源的影响是相同的
  • 幂等操作的特性是:无副作用(No Side Effect)

📌 二、幂等性为什么重要?

会出现重复调用的原因:
  • 网络抖动、页面重复点击
  • 分布式事务重试机制
  • 网关重试、MQ重复投递
  • 前端误操作(如双击支付按钮)

🎯 三、幂等性保障方案(重点)

场景 适用范围 实现方式 特点
✅ 数据库唯一约束 新增接口 通过唯一索引防止重复插入 简单高效
✅ Token机制 + Redis 提交类(订单、支付) 一次性Token防止重复提交 推荐,效率高
✅ 分布式锁 修改、转账等接口 基于Redisson、ZK等实现同步控制 精准,但性能稍差
✅ 接口幂等表 所有需幂等接口 记录请求唯一标识 + 状态 控制最强,适合高并发
✅ 乐观锁(版本号机制) 更新操作 防止并发更新,CAS思想 要求带版本号字段
✅ 幂等组件中间件 通用接口平台 拦截层统一幂等处理 企业封装,灵活可控

🔨 Token+Redis 实现幂等(推荐)

流程:

  1. 客户端请求创建订单,先调用接口获取 幂等Token
  2. 后续提交时在请求头中携带该 Token
  3. 服务端收到请求:
    • 判断 Redis 中是否存在该 Token
    • 若存在,则处理业务并删除 Token
    • 若不存在,说明已处理或重复请求,直接返回

优点:

  • 轻量级,无锁高并发
  • 可扩展性强,适用于 POST/PUT 等需要控制的接口

幂等多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致

需要幂等场景
  • 用户重复点击(网络波动)
  • MQ消息重复
  • 应用使用失败或超时
请求方式 说明 是否天然幂等
GET 查询操作,天然幂等 ✅ 是
POST 新增操作,请求一次与请求多次造成的结果不同,不是幂等的 ❌ 否
PUT 更新操作,如果是以绝对值更新,则是幂等的。如果是通过增量的方式更新,则不是幂等的 ✅ 是(全量)或❌ 否(增量)
DELETE 删除操作,根据唯一值删除,是幂等的 ✅ 是(按主键)
-- 幂等更新:将余额设置为固定值
UPDATE account SET money = 500 WHERE id = 1;

-- 非幂等更新:余额加上500
UPDATE account SET money = money + 500 WHERE id = 1;
  • 数据库唯一索引【新增】

  • ==token + redis== 【新增+修改】**AND** ==分布式锁== 【新增+修改】

    🧠 面试答题思路模板

    我们项目中对于需要幂等性的接口(如支付、下单等),主要采用了Token + Redis机制

    • 请求前由前端向后端申请一次性 Token
    • 后续接口请求中携带该 Token
    • 后端通过 Redis 判断 Token 是否存在,从而保证接口只被处理一次

    此外,对于批量创建类操作,还会结合数据库唯一索引控制幂等,对于状态更新类操作会使用乐观锁分布式锁

    创建商品、提交订单、转账、支付等操作

你们项目中使用了什么分布式任务调度

xxl-job 是一个分布式任务调度平台,它致力于解决分布式场景下的任务调度问题,主要由调度中心和执行器两部分组成。调度中心负责统一管理任务调度,而执行器则是负责接收调度并执行任务逻辑的客户端。

🎯 一句话总结

我们项目使用的是 XXL-Job 分布式任务调度平台,用来实现定时任务管理,比如取消订单、同步库存、发送通知等,解决了分布式环境下定时任务重复、不可控的问题。

  • xxl-job路由策略有哪些?

    xxl-job提供了很多的路由策略,我们平时用的较多的就是:轮询、故障转移、分片广播

    🚦 路由策略有哪些?(面试重点)

    路由策略就是调度中心选 哪个执行器节点去执行任务 的方式。

    路由策略 描述
    轮询(Round) 多个节点轮流执行任务,负载均衡常用
    故障转移(Failover) 优先使用健康节点,失败时自动切换执行
    分片广播(Broadcast) 每个节点都执行一次任务,适合大数据并行处理
    随机(Random) 随机选取一个可用节点
    一致性哈希 根据任务参数哈希选节点,适合状态保持
    最少运行节点 选当前执行任务最少的机器
    指定机器 手动指定执行器
  • xxl-job任务执行失败怎么解决?

    • 路由策略选择故障转移,使用健康的实例来执行任务
    • 设置重试次数
    • 查看日志+邮件警告来通知相关负责人解决
    • 配置 任务超时时间 + 告警通知(邮件、短信)
    • 失败日志可在调度中心查看,有堆栈信息
  • 如果有大数据量的任务同时都现需要执行,怎么解决?

    • 让多个实例一块去执行(部署集群),路由策略分片广播
    • 在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行;通过 ShardingContext 拿到当前分片信息
    @JobHandler("xxxHandler")
    public ReturnT<String> execute(ShardingContext context) {
        int totalShards = context.getTotalShardCount();
        int shardIndex = context.getShardIndex();
    
        List<Data> allData = getData();
        for (int i = 0; i < allData.size(); i++) {
            if (i % totalShards == shardIndex) {
                process(allData.get(i));
            }
        }
        return SUCCESS;
    }
    

💡 面试高频问法总结

面试问题 回答提示
你们怎么做定时任务调度? 我们使用 XXL-Job 做分布式定时调度,支持失败重试、分片执行、失败告警
XXL-Job 怎么实现任务分片? 配置为广播策略,任务中使用 ShardingContext 获取分片信息
路由策略都有哪些?你们用哪个? 常用轮询、故障转移、分片广播;我们大任务用广播,小任务轮询
如果一个任务执行失败怎么办? 设置了最大重试次数、使用了 Failover 策略、并配置了邮件告警通知
为什么不用 @Scheduled? @Scheduled 适用于单体项目,不能解决多实例下重复执行问题;XXL-Job 支持分布式调度、管理界面、执行日志等

✅ 详细对比分析:SpringTask vs XXL-Job

对比维度 SpringTask XXL-Job
定位 本地轻量级定时调度工具 分布式定时任务调度平台
部署方式 内嵌在应用中(@Scheduled) 独立部署调度中心 + 执行器
集群支持 不支持,单实例任务 支持分布式执行和 failover
任务失效恢复 无(如服务挂了任务丢) 支持失败重试 + 调度日志记录
任务管理 无可视化界面 Web 界面管理、动态配置任务
任务执行方式 方法注解 + 固定周期 支持 Bean 调用、Shell、HTTP、RPC 等
执行结果监控 有日志管理、失败报警、状态追踪
调度策略 固定时间(cron) cron、分片广播、失败重试、手动触发等
适用场景 简单、稳定的定时逻辑,如定期清理缓存 多任务调度、跨服务控制、任务分发、分片执行、手动补偿等复杂场景
xxl-job解决的问题
  • 解决集群任务的重复执行问题 xxl-job路由策略有哪些?
  • cron表达式定义灵活在页面上 xxl-job任务执行失败怎么解决?
  • 定时任务失败了,重试和统计 如果有大数据量的任务同时都需要执行,怎么解决?
  • 任务量大,分片执行

场景 1: 定时处理过期订单

假设用户下单后如果订单超过了某个时间没有支付,平台需要自动取消该订单并释放库存。这个任务需要在每天的某个固定时间(比如凌晨 2 点)运行。

解决的问题:
  1. 定时任务调度:XXL-Job 可以轻松管理该任务的执行时间和周期,确保每天准时执行,不需要开发者手动触发。
  2. 任务失败重试:如果该任务因为某些原因执行失败,XXL-Job 可以自动进行重试,并设置重试次数,确保任务最终被执行。
  3. 分布式执行:假设电商平台是一个分布式系统,订单数据存储在多个数据库中,XXL-Job 可以通过分布式执行确保每个数据库的订单都被正确处理

假设每晚 2 点有一个任务需要取消未支付的订单

public class OrderJob {
    @JobHandler("orderCancelJobHandler")
    public void cancelUnpaidOrders() {
        // 查询所有未支付的订单
        List<Order> unpaidOrders = orderService.findUnpaidOrders();
        for (Order order : unpaidOrders) {
            if (order.isExpired()) {
                orderService.cancelOrder(order);
                inventoryService.releaseStock(order.getProductId(), order.getQuantity());
                // 发送订单取消通知给用户
                notificationService.sendOrderCancelledNotification(order.getUserId());
            }
        }
    }
}

场景 2: 定时更新商品库存

假设电商平台上销售的是一些有时效性的商品,商家需要定期更新商品的库存状态(例如,库存数量达到一定阈值时,自动下架商品,或者增加库存数量)。这个任务同样需要定时执行。

解决的问题:
  1. 任务分片:在商品很多的情况下,XXL-Job 可以通过任务分片的方式并行处理不同商品的库存更新,提升任务的执行效率。
  2. 任务优先级:根据不同商品的重要程度,XXL-Job 可以设置任务的优先级,确保关键商品的库存更新优先执行。
public class InventoryJob {
    @JobHandler("inventoryUpdateJobHandler")
    public void updateProductInventory() {
        // 获取需要更新库存的商品
        List<Product> productsToUpdate = productService.findProductsForInventoryUpdate();
        for (Product product : productsToUpdate) {
            inventoryService.updateInventory(product);
            if (product.getStockQuantity() <= product.getLowStockThreshold()) {
                productService.deactivateProduct(product);
                // 发送商品下架通知
                notificationService.sendOutOfStockNotification(product.getId());
            }
        }
    }
}

场景 3: 定时发送促销活动通知

假设电商平台有一个促销活动,每个活动的开始和结束时间都由后台系统控制。需要在活动开始前 1 小时、活动结束时发送通知给用户。这些通知可以是短信、邮件或 APP 推送通知。

解决的问题:
  1. 定时任务管理:XXL-Job 可以定时触发通知任务,确保用户在活动前后及时收到通知。
  2. 高并发支持:在促销活动开始或结束时,平台可能会有大量的通知需要发送,XXL-Job 支持任务的并行处理,可以帮助我们高效地分发通知,避免性能瓶颈。
  3. 任务状态监控:XXL-Job 提供任务的实时监控功能,平台可以随时查看任务的执行情况,确保通知任务按时执行。
public class PromotionJob {
    @JobHandler("promotionNotifyJobHandler")
    public void sendPromotionNotifications() {
        // 获取当前正在进行的促销活动
        List<Promotion> activePromotions = promotionService.findActivePromotions();
        for (Promotion promotion : activePromotions) {
            if (promotion.isStartingSoon()) {
                notificationService.sendStartNotification(promotion);
            } else if (promotion.isEndingSoon()) {
                notificationService.sendEndNotification(promotion);
            }
        }
    }
}

✅ 这段代码做了什么?

java复制编辑@JobHandler("orderCancelJobHandler")
public void cancelUnpaidOrders() { ... }

这是一个 XXL-Job 的定时任务处理方法,任务名叫 orderCancelJobHandler,作用是:

定时扫描所有未支付订单,判断是否过期,过期则取消订单、释放库存并通知用户


🔧 XXL-Job 是怎么运行这个方法的?

🧩 步骤 1:添加依赖

在你的 pom.xml 中,需要加入 XXL-Job 的执行器依赖:

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.4.0</version>
</dependency>

🧩 步骤 2:执行器配置(application.yml)

xxl:
  job:
    admin:
      addresses: http://localhost:8080/xxl-job-admin
    executor:
      appname: order-job-executor
      address:
      ip:
      port: 9999
      logpath: /data/applogs/xxl-job/jobhandler
      logretentiondays: 30
    accessToken:

说明:

  • admin.addresses: 调度中心地址
  • executor.*: 当前服务是执行器,配置端口、日志路径等

🧩 步骤 3:启动类开启执行器功能

@EnableXxlJob
@SpringBootApplication
public class OrderJobExecutorApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderJobExecutorApplication.class, args);
    }
}

🧩 步骤 4:创建 JobHandler 并注册到容器

@Component
public class OrderJob {

    @XxlJob("orderCancelJobHandler")
    public void cancelUnpaidOrders() {
        // 执行任务逻辑
    }
}

注意:你用的是 @JobHandler,它是旧版本用法,新版本统一改为 @XxlJob(推荐)


🧩 步骤 5:在调度中心页面注册该任务

登录调度中心(xxl-job-admin)

  • 新建任务
    • 执行器:选择 order-job-executor
    • JobHandler名称:orderCancelJobHandler
    • 调度类型:如“CRON”
    • 执行方式:BEAN 模式
    • CRON 表达式:比如每天凌晨 2 点 0 0 2 * * ?
  • 保存并启动任务
xxl-job路由策略有哪些?

实例找任务项执行任务 这种找机器的方式就是路由策略

消息中间件RabbitMQ+Kafka

消息中间件提供了服务与服务之间的异步调用,还可以服务与服务之间解耦

RabbitMQ:**消息不丢失、消息重复消费、消息堆积、延迟队列、死信队列、高可用机制**
Kafka:**消息不丢失、消息重复消费、高可用机制、高性能设计吞吐量达到百万级数据存储和清理**

RabbitMQ-如何保证消息不丢失?

  • 开启生产者确认机制,确保生产者的消息能到达队列
    confirm到交换机ack 不到nack 和 return没到返回nack机制保证生产者把消息发过去

    达到队列成功返回ack,失败返回nacknegative acknowledgment

    1. 生产者发送消息到交换机。
    2. 交换机收到消息后,根据绑定规则(是否有匹配的队列)决定消息是否被正确路由。
    3. 如果消息成功路由到队列,交换机会向生产者返回 ack 确认。
    4. 如果消息没有成功路由到任何队列,交换机会通过 return 将消息退回给生产者。
    5. 生产者收到 acknack,可以处理消息确认或重试逻辑。
  • 开启持久化功能,确保消息未消费前在队列中不会丢失 durable = True
    万一broker挂掉就惨了 保证至少成功一次消费
    MQ是默认内存存储信息,开启持久化功能可以确保缓存在MQ中的消息不丢失[把数据存在磁盘上]

    # 声明持久化交换器
    channel.exchange_declare(exchange='exchange_name', durable=True)
          
    # 声明持久化队列
    channel.queue_declare(queue='queue_name', durable=True)
          
    # 发送持久化消息
    channel.basic_publish(exchange='exchange_name',
                          routing_key='routing_key',
                          body='Hello World!',
                          properties=pika.BasicProperties(
                             delivery_mode=2,  # 使消息持久化
                          ))
    
  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
    消费者三种机制:

    RabbitMQ支持消费者确认机制,即:**消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息**,而Spring AMQP则允许配置三种确认模式:

    • manual:手动ack,需要在业务代码结束后,调用api发送ack。

    • auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

    • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

  • 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理

    在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

    • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

    • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

    • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

  • 异步发送(验证码、短信、邮件)
  • MySQL和Redis,ES之间的数据同步
  • 分布式事务
  • 削峰填谷
==生产者确认机制==

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功

RabbitMQ消息的重复消费问题如何解决的?

我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息,这样就重复消费了
因为我们当时处理的支付(订单|业务唯一标识),它有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了

原因
  • 网络抖动
  • 消费者挂了

解决方案:适用于任何MQ(Kafka,RabbitMQ,RocketMQ)

  • 每条消息设置一个唯一的标识id
  • 幂等方案:【分布式锁、数据库锁(悲观锁、乐观锁)】

RabbitMQ中死信交换机?(RabbitMQ延迟队列有了解过吗)

如果用原来的定时任务 也可以但是 可能会有订单空窗期 如果没人消费的时候 它内部还是回去sql查询已下单 +(now()-下单时间)?15min : true, false

  • 我们当时一个什么业务使用到了延迟队列(超时订单、限时优惠、定时发布)
  • 其中延迟队列就用到了死信交换机TTL(消息存活时间)实现的
  • 消息超时未消费就会变成死信(死信的其他情况:拒绝被消费,队列满了

我们用延迟队列处理未支付订单:用户下单后 15 分钟未付款,自动取消订单+释放库存;其他场景也用到延迟队列,比如限时促销、定时发布文章、定时发通知等。

延迟队列插件实现延迟队列DelayExchange
延迟队列的底层实现就是通过 TTL(消息存活时间)+ 死信交换机(DLX)组合完成的。

  • 声明一个交换机,添加delayed属性为true
  • 发送消息时,添加x-delay头,值为超过时间
什么样的消息会成为死信?
★ 消费者返回reject或者nack,且requeue参数设置为false【消息被拒绝并且不重入队列】
★ 消息超时未消费
★ 队列满了

如何给队列绑定死信交换机?
★ 给队列设置dead-letter-exchange属性,指定一个交换机
★ 给队列设置dead-letter-routing-key属性,设置死信交换机与死信队列的RoutingKey

------------------------------------------------------------------------
★ ★ ★ 使用 Spring AMQP 配置 ★ ★ ★ 
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    // 定义普通队列
    @Bean
    public Queue normalQueue() {
        return new Queue("normalQueue", true, false, false, 
                Map.of("x-dead-letter-exchange", "dlx_exchange", 
                       "x-dead-letter-routing-key", "dlx_routing_key"));
    }

    // 定义死信队列
    @Bean
    public Queue dlxQueue() {
        return new Queue("dlxQueue", true);
    }

    // 定义普通交换机
    @Bean
    public Exchange normalExchange() {
        return new TopicExchange("normal_exchange");
    }

    // 定义死信交换机
    @Bean
    public Exchange dlxExchange() {
        return new TopicExchange("dlx_exchange");
    }

    // 将普通队列与交换机绑定
    @Bean
    public Binding bindNormalQueue() {
        return BindingBuilder.bind(normalQueue()).to(normalExchange()).with("normal.routing.key").noargs();
    }

    // 将死信队列与死信交换机绑定
    @Bean
    public Binding bindDLXQueue() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx_routing_key").noargs();
    }
}
------------------------------------------------------------------------
如果你希望将死信队列配置成带有过期时间或其他特殊属性的队列,可以在定义 dlxQueue 时增加更多的设置,例如 TTL(过期时间)。
例如,设置死信队列的 TTL:

@Bean
public Queue dlxQueue() {
    return QueueBuilder.durable("dlxQueue")
            .withArgument("x-message-ttl", 60000) // 设置TTL为60秒
            .build();
}
✅ Spring AMQP 中配置延迟队列 + 死信队列(简洁版)

@Bean
public Queue orderQueue() {
    return QueueBuilder.durable("order.queue")
            .ttl(15 * 60 * 1000) // 15分钟TTL
            .deadLetterExchange("dlx.exchange") // 超时后投递到死信交换机
            .deadLetterRoutingKey("order.dlx")  // 指定死信路由key
            .build();
}

@Bean
public Queue orderDLXQueue() {
    return new Queue("order.dlx.queue");
}
延迟队列 = 死信交换机 + TTL (生存时间)
  • 延迟队列:进入队列的消息会被延迟消费的队列
  • 场景:超时订单、限时优惠,定时发布
死信交换机

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.reject 或 basic.nack声明消费失败,并且信息的requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信

如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)

@Bean
public QUeue ttlQueue(){
    return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
        .ttl(10000) // 设置队列的超时时间 10秒
        .deadLetterExchange("dl.direct") // 指定死信交换机
        .build();
}
✅ 死信交换机配置核心属性(面试考点)

x-dead-letter-exchange: 死信交换机名称
x-dead-letter-routing-key: 投递死信用的路由key
x-message-ttl: 消息过期时间(单位:毫秒)
TTL

TTL(Time-To-Live)。如果一个队列中的消息TTL结束仍未消费,则会变成死信,ttl超时分为两种情况:

  • 消息所在的队列设置了存活时间
  • 消息本身设置了存活时间

死信图片

RabbitMQ如果有100万消息堆积在MQ,如何解决(消息堆积怎么解决)?

消息堆积的本质是消费能力不足,解决重点是提升消费者处理能力 + 降低系统压力。

**解决消息堆积有三种思路 **

  • 增加更多消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限
    • 在声明队列的时候可以设置x-queue-model为lazy,即为惰性队列
    • 基于磁盘存储,消息上限高
    • 性能比较稳定,但基于磁盘存储,受限于磁盘IO,时效性会降低

生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是堆积问题

**解决消息堆积有三种思路 **

  • 增加更多消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限
达到上限发送的消息会变成死信,那我为什么不搞个死信交换机 而是用了上面的三种思路??

使用死信交换机(DLX, Dead Letter Exchange)是另一种处理消息堆积的方式,但它的作用更偏向于“消息过期”或“处理失败”的情况下将消息转发到另一个队列。并不直接解决生产者发送消息过快或消费者处理速度过慢的问题。通过死信交换机,你可以将无法处理的消息转发到其他队列,方便你后续进行分析或处理,但它并不能提高消费者处理消息的速度。

针对消息堆积的本质问题,解决方式更多的是优化消费者处理能力,而不是仅依赖死信交换机。具体而言,死信交换机和你的三种思路的关系如下:

  1. 死信交换机(DLX):当消息达到队列上限或无法消费时,消息被转发到死信队列。你可以分析死信队列中的消息,了解原因,并决定是重试、丢弃还是进行其他处理。它的作用是不丢失消息,但并不能帮助消除堆积。
  2. 增加消费者:这是直接针对堆积的根本解决方案,通过增加消费者数量来加速消息的处理。死信交换机无法直接解决消费者处理能力不足的问题。
  3. 开启消费者线程池:在单个消费者上开启线程池,可以提高消费者的处理能力,减少堆积。死信交换机并不能增加消息处理速度,它只是用来应对消费失败的情况。
  4. 扩大队列容量并使用惰性队列:惰性队列可以将消息存储在磁盘上,而非内存中,减轻内存压力,但这也会降低时效性,并不能解决生产者生产过快或消费者消费过慢的问题。死信交换机同样无法直接解决这一点。

总结来说,死信交换机是处理消息丢失或无法消费的方式,它和通过增加消费者、线程池、队列优化这些手段并不冲突,但也无法替代这些更直接的解决方案。你可以结合这两者,使用死信交换机来保障消息不丢失,同时采取上述方法来提高消息消费速度。

死信交换机主要作用是保底机制,用于处理失败的消息,比如超时、拒绝、队列满。它不能真正解决堆积问题,只能帮我们兜底不丢消息
所以,我们会搭配使用:前面是用多线程和惰性队列来压堆积,后面用死信队列来兜住异常消息,保障系统稳定。

惰性队列

惰性队列特征如下:

  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持数百万条的消息存储
@Bean
public Queue lazyQueue(){
    return QueueBuilder
            .durable("lazy.queue")
            .lazy() // 设置为惰性队列,落盘存储
            .build();
}

// 惰性队列是 RabbitMQ 提供的一种机制,消息直接写入磁盘,内存消耗小,堆积能力强,适合处理海量不紧急消息。但缺点是消费速度会稍慢,因为要从磁盘读。
@RabbitListener(queuesToDeclare = @Queue){
    name = "lazy.queue",
    durable = "true",
    arguments = @Argument(name = "x-queue-mode"), value="lazy"
}
public void listenLazyQUeue(String msg){
    log.info("接收到lazy.queue的消息:{}",msg);
}

提高消费能力是解决堆积的根本;惰性队列能缓解内存压力,死信机制是辅助保障不丢消息。三者应配合使用。

RabbitMQ高可用机制有了解过吗? &&  请描述 RabbitMQ 镜像队列的工作原理及其在高可用性场景下的优缺点

在我们项目中,为了保障消息队列的高可用性,我们采用了 RabbitMQ 的镜像队列集群部署方案,后来也了解并测试了仲裁队列来提升一致性保障。

我们当时的项目在生产环境下,采用的是镜像模式搭建的集群,共有3个节点
镜像队列结构是一主多从(从就是镜像),所有(写)操作都是主节点完成,然后同步给镜像节点
主宕机后,镜像节点会代替成为新的主(如果在主从同步完成前,主就已经宕机,可能出现数据丢失)

🔹 RabbitMQ 高可用机制对比三种:
模式 原理 是否同步消息 容灾能力 是否强一致 应用场景
普通集群 共享元数据,不同步消息内容 测试、非关键业务
镜像队列 主节点写入,同步到镜像节点 弱~中 读多写少
仲裁队列 Raft 协议选主,多节点确认写入 ✅(强一致) 关键数据、支付

那出现丢数据怎么解决呢?

我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于wwwwwwwwwwwwwwwwwwwww,强一致性,并且使用起来也非常简单,不需要格外的配置,在声明队列的时候只需要指定这个是仲裁队列即可

Raft 协议 是一种 共识算法(Consensus Algorithm)
🔴 在分布式系统中,让多个节点就某个值达成一致(即使有部分节点故障)
Raft 就是“几个节点如何选出一个 Leader,让它来统筹所有更新,然后让大多数节点都确认成功后再算真正提交成功”。
为了避免这些 “脑裂、数据冲突” 问题,就需要一种 “大家达成共识” 的机制,Raft 协议就是这种机制🏗️ Raft 核心角色和流程

✅ Raft 有三种角色:

角色 说明
Leader 主节点,唯一能接收客户端写请求
Follower 跟随者,接受 Leader 同步数据
Candidate 候选人,用于选举新 Leader

✅ 正常流程:

  1. 所有节点刚启动时,都是 Follower。
  2. 一段时间内没收到 Leader 的心跳,会变成 Candidate,发起投票选举。
  3. 多数投票成功后,变成新的 Leader。
  4. 所有写请求只能由 Leader 处理,然后同步给其他节点(Follower)。
  5. 如果 大多数节点都确认写入成功,则这条数据才算真正提交成功。
  • 在生产环境下,使用集群来保证高可用性
  • 普通集群、镜像集群、仲裁队列(Raft协议)
普通集群

普通集群,或者叫标准集群(classic cluster)

  • 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的信息
  • 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
  • 队列所在节点宕机,队列中的消息就会丢失
镜像集群

镜像集群:本质是主从模式,具备下面的特征:

  • 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份
  • 创建队列的节点被称为该队列的主节点,备份到的其他节点叫做该队列的镜像节点
  • 一个队列的主节点可能是另一个队列的镜像节点
  • 所有操作都是主节点完成,然后同步给镜像节点
  • 主宕机后,镜像节点会替代成新的主

🔹 镜像队列原理(项目中实际使用)

在我们的生产环境中,RabbitMQ 集群采用的是 镜像队列(Classic Mirrored Queue) 模式,有以下特点:

  • 主从同步: 队列存在一个主节点,多个镜像节点。
  • 所有写操作都落在主节点,随后同步给镜像节点
  • 如果主节点宕机,镜像节点会被选举为新主。
  • 同步存在延迟,一旦主节点宕机且尚未同步完,就可能出现数据丢失

💡 镜像队列优点:

  • 数据有副本,具备一定容灾能力
  • 节点可切换,保证服务不中断

⚠ 镜像队列缺点:

  • 主从同步可能延迟 → 存在数据丢失风险
  • 配置复杂、资源开销大,大集群下性能差

🔹 那出现丢数据怎么办?

我们后来开始测试和使用了 仲裁队列(Quorum Queue),它在 3.8+ 中推出,作为镜像队列的替代方案,支持强一致性

仲裁队列:.quorum()

仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列

  • 与镜像队列一样,都是主从模式,支持主从数据同步
  • 使用非常简单,没有复杂的配置
  • 主从同步基于Raft协议,强一致性

仲裁队列的工作原理如下:

  1. 主从模式:仲裁队列也是主从模式,支持主从数据同步。
  2. Raft 协议:主从同步基于 Raft 协议,确保数据的一致性和可靠性。
  3. 强一致性:所有写操作必须得到大多数节点的确认后才能完成,避免了数据丢失。

仲裁队列通过以下机制保证数据不丢失:

  • 多数派确认每次写操作需要得到大多数节点的确认,确保数据已经成功复制到多个节点
  • 自动故障转移:如果主节点宕机,Raft 协议会自动选举新的主节点,确保服务的连续性。
  • 数据一致性Raft 协议保证了数据的强一致性,即使在网络分区或节点宕机的情况下,也不会出现数据不一致的问题。

仲裁队列的优点是配置简单、数据强一致,但需要至少 3 个节点,并且在写操作上的延迟和资源消耗可能会比镜像队列高。

@Bean
public Queue quorumQueue(){
    return QueueBuilder
            .durable("quorum.queue") // 持久化
            .quorum() // 仲裁队列
            .build();
}

Kafka是如何保证消息不丢失?

需要从三个层面去解决这个问题

  • 生产者发送消息到Brocker丢失

    • 设置异步发送,发送失败使用回调进行记录或重发
    • 失败重试,参数配置,可以设置重试次数消息
  • 在Brocker中存储丢失

    发送确认acks,选择all,让所有的副本都参与保存数据后确认

  • 消费者从Brocker接收消息丢失

    • 关闭自动提交偏移量,开启手动提交偏移量
    • 提交方式:最好是同步+异步提交

使用Kafka在消息的收发过程中都会出现消息丢失,Kafka分别给出了解决方案

  • 生产者发送消息到Brocker丢失
  • 消息在Brocker中存储丢失
  • 消费者从Brocker接收消息丢失

kafka-高产出的分布式消息系统(A high-throughput distributed messaging system)。

Kafka是一个高吞吐、分布式、基于发布订阅的消息系统,利用Kafka技术可以在廉价的PC Server上搭建起大规模消息系统。

Kafka的特性:
  • 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作;
  • 可扩展性:kafka集群支持热扩展;
  • 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失;
  • 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败);
  • 高并发:支持数千个客户端同时读写;
  • 支持实时在线处理和离线处理:可以使用Storm这种实时流处理系统对消息进行实时进行处理,同时还可以使用Hadoop这种批处理系统进行离线处理;

Kafka和其他组件比较,具有消息持久化、高吞吐、分布式、多客户端支持、实时等特性,适用于离线和在线的消息消费,如常规的消息收集、网站活性跟踪、聚合统计系统运营数据(监控数据)、日志收集等大量数据的互联网服务的数据收集场景。

  1. 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如Hadoop、Hbase、Solr等;
  2. 消息系统:解耦和生产者和消费者、缓存消息等;
  3. 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到Hadoop、数据仓库中做离线分析和挖掘;
  4. 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告;
  5. 流式处理:比如spark streaming和storm;
  6. 事件源;
  7. kafka在FusionInsight中的位置:

Kafka是如何保证消费的顺序性?

Kafka 保证消息不丢失需要从 生产者、Broker、消费者 三个维度考虑:

  1. 生产者侧:通过设置 acks=all、启用 幂等性(enable.idempotence=true)、配置 重试机制 等,确保消息可靠送达 Kafka。
  2. Broker 侧:通过消息持久化、副本同步机制(ISR)、故障转移机制等,确保存储端可靠。
  3. 消费者侧:通过 关闭自动提交 offset,使用手动同步/异步提交,并搭配幂等消费逻辑,避免消息处理丢失。

多项机制结合,共同保障 Kafka 在高吞吐、高并发下仍具备良好的可靠性与稳定性。

问题原因:
一个topic的数据可能存储在不同的分区中 ,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性

==解决方案:==

  • 发送消息时指定分区号
  • 发送消息时按照相同的业务设置相同的key

应用场景:

  • 即时消息中的单对单聊天和群聊,保证发送方消息发送顺序与接收方的顺序一致
  • 充值转账两个渠道在同一个时间进行金额变更,短信通知必须要有顺序

承接上图消费者从Brocker接收消息丢失
如何做?→ topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。所以,如果你想要顺序的处理Topic的所有消息,那就只提供一个分区。

// 指定分区
kafkaTemplate.sent("springboot-kafka-topic",0,"key-001","value-001");
// 相同的业务key
kafkaTemplate.sent("springboot-kafka-topic","key-001","value-001");

会计算key的hashcode值推断出它在哪个分区,如果要求有顺序性 就可以设置同一个key,此时hash值都是一样的 就可以在同一个分区存储

Kafka的高可用机制有了解过吗?

  • ==集群模式==

    一个kafka集群由多个broker实例组成,即使某一台宕机,也不会耽误其他broker继续对外提供服务

  • ==分区备份机制==

    • 一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中
    • 所有的分区副本的内容都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性
✅ 真实业务场景案例:订单创建消息流的高可用设计(基于 Kafka)
🟡 业务背景

你在做一个电商平台系统,其中订单创建之后,需要进行如下异步操作:

  1. 通知库存中心扣减库存
  2. 通知积分服务赠送用户积分
  3. 通知数据中心写入大数据平台(如 HDFS)

为了解耦服务、削峰填谷,并保障消息不丢失,我们决定使用 Kafka 进行消息中转。


🟧 Kafka 高可用设计
▶ 架构图概览(文字版)
[订单服务 OrderService] 
    |
    | 生产消息 send(order_id=1001)
    ▼
[Kafka集群 topic=order-create partition=0,1,2]        
    |  partition-0:leader 在 broker1,follower 在 broker2,3
    |  partition-1:leader 在 broker2,follower 在 broker1,3
    |  partition-2:leader 在 broker3,follower 在 broker1,2
    ▼
[消费者集群]
  ├── 库存服务(group=stock)
  ├── 积分服务(group=points)
  └── 数据服务(group=bigdata)
🔵 Kafka 高可用机制在这里怎么体现?
✅ 1. Kafka 集群部署(Broker 多节点)

部署 3 个 Kafka Broker:

broker.id=1,2,3 分别配置在三台服务器上

即便其中任意一台 Broker 宕机,剩下两台仍然可以继续服务。

✅ 2. Topic 分区 + 副本机制

创建 topic 时设定副本数:

bin/kafka-topics.sh --create \
  --topic order-create \
  --partitions 3 \
  --replication-factor 3 \
  --zookeeper zk1:2181
  • 每个分区会有 1 个 leader + 2 个 follower

  • Leader 负责读写,follower 同步数据

  • 当 Leader 所在 Broker 宕机时,Kafka 会自动从 ISR 列表中选一个 follower 升级为 leader
    🔁 例如:

  • 原来 partition-0 的 leader 是 broker1

  • broker1 崩了 → Kafka Controller 会选 broker2 或 broker3 提升为新 leader

✅ 3. 生产者配置 acks=all + 幂等性

为了保障消息可靠写入 Kafka:

props.put("acks", "all"); // 所有副本都确认才算成功
props.put("enable.idempotence", "true"); // 避免重复投递

📝 所以生产者即便 retry 了,也不会生成重复消息。

✅ 4. 消费者使用手动提交 offset

为了保证消费者处理完消息再提交 offset,避免消息处理失败却误提交偏移量:

props.put("enable.auto.commit", "false");

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 处理消息
        process(record);
    }
    consumer.commitSync(); // 处理完手动提交
}
🔻 模拟突发事件:Kafka Broker 宕机,系统是否正常?
❓ 假设 Broker1 宕机,会发生什么?
  • partition-0 的 leader 在 broker1 → Kafka Controller 自动将 broker2 提升为新的 leader
  • 生产者发送消息依然正常(因为 follower 已同步数据)
  • 消费者照常消费 partition-0 的数据

✅ 总结一波这个真实案例带来的知识点

组件 高可用机制 关键作用
Kafka 集群 多 Broker + Controller 任一节点挂掉不影响整体服务
Topic 分区副本 leader + follower 保证消息副本安全、自动故障转移
Producer acks=all + 幂等性 防止生产过程中消息丢失或重复
Consumer 手动提交 offset 避免处理失败误提交偏移量

❓面试模拟追问

面试官:你们 Kafka 的 leader 挂了会不会影响消费者?

你可以回答:

Kafka 的消费者是从分区的 leader 拉取消息的,如果 leader 节点挂了,Kafka 会自动从 ISR 列表中挑选新的 follower 作为 leader,整个过程对消费者来说是无感知的,消费过程会自动恢复。

解释一下复制机制中的ISR?

ISR (in-sync replica) 需要同步复制保存的follower;跟 Leader 保持同步 的副本集合
分区副本分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉后,会优先从ISR副本列表中选取一个作为leader

Kafka 每个分区(Partition)都会有多个副本(Replicas):

  • 一个 Leader
  • 若干个 Follower

这些副本被分成两类:

类型 描述
ISR 副本 与 Leader 保持数据同步的副本(最健康的副本集
OSR(Out-of-Sync Replicas) 落后太多,未能及时同步 Leader 数据的副本

🧠 工作机制详解

1️⃣ 消息写入

  • Producer 只写 Leader
  • Leader 负责将数据同步到 ISR 中的所有 Follower
  • 所有 ISR 成员都成功写入后,才向 Producer 发送 ack

如果 Leader 挂了:

  • Kafka 只会从 ISR 列表中挑选一个副本晋升为新的 Leader
  • 这样可以确保新 Leader 中的数据是完整的

🟩 举个例子助记

假设 topic 有 3 个副本:

Partition-0:
  - broker1 (Leader)
  - broker2 (Follower)
  - broker3 (Follower)

此时,**ISR = [broker1, broker2]**(broker3 落后太多,暂不在 ISR 中)

如果 broker1 挂掉了 → Kafka 会从 broker2 中选一个当 Leader(因为它是同步副本)

📌 broker3 不在 ISR 中,就算数据多,也不会被选为 leader(因为可能数据不一致)


Kafka 中每个分区都会有多个副本,其中 ISR(In-Sync Replicas)表示与 Leader 保持同步的副本集合。

ISR 是 Kafka 高可用的重要保障,Kafka 只会从 ISR 中选 leader,避免使用落后副本导致数据丢失。

可以通过配置 min.insync.replicas 来控制 ISR 最小数量,从而配合 acks=all 确保数据写入可靠性。

// 一个topic默认分区的replication个数,不能大于集群中broker的个数。默认为1
default.replication.factor=3
// 最小的ISR副本个数
min.insync.replicas=2

Kafka数据清理机制了解过吗?

  • kafka文件存储机制
    • Kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment
    • 每个分段都在磁盘上以索引(xxxx.index)和日志文件(xxx.log)的形式存储
    • 分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理
  • 数据清理机制
    • 根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认168小时(7天)
    • 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息(默认关闭)

Kafka 和 RabbitMQ 对比

对比维度 Kafka RabbitMQ
核心模型 发布-订阅模型(Pub-Sub) 基于消息队列(Queue)
架构设计 分布式、高吞吐、日志存储 面向消息、消息中间人
消息存储 持久化日志,磁盘为主,保留时间可配 内存为主,结合磁盘,可配置 TTL
性能吞吐量 非常高(百万级) 相对较低(万级)
消息顺序性 同一个分区内有序 默认无序(队列先进先出 FIFO)
消息重复消费 可能(需要幂等性) 通过消息确认机制避免
消息确认机制 Offset 手动提交或自动提交(可恢复) Ack 机制、可配置手动/自动确认
可靠性机制 副本机制 + ISR 保证 持久化、确认机制、死信队列、重试机制
高可用性 分区副本机制 + Leader选举(Raft) 镜像队列或仲裁队列
消息丢失保护 acks=all + min.insync.replicas confirm机制 + durable队列 + ack
延迟队列支持 原生不支持(需要定时调度或借助外部插件) 原生支持延迟队列(TTL + 死信队列)
使用场景 日志采集、行为跟踪、数据管道、实时流处理 任务异步处理、延迟任务、订单超时处理等
管理界面 UI界面较弱,需要依赖第三方 自带 Web 管理后台,功能强大
学习曲线 略陡峭,需要理解 partition、offset、consumer group 等 简单易上手,文档丰富
协议支持 Kafka 自有协议 支持 AMQP、MQTT、STOMP 等多种协议
成熟度 大数据生态首选,社区活跃,企业常用 轻量级场景常用,灵活扩展,适合中小型应用

🧠 总结一句话对比

  • 🔥 Kafka 更适合高吞吐、日志/流式处理、大数据实时系统
  • 🔧 RabbitMQ 更适合业务系统间的解耦、任务异步处理、延迟消息机制

Kafka 和 RabbitMQ 都是消息中间件,但定位不同:

  • Kafka 是高吞吐、分布式日志系统,更适合大数据场景和实时流式处理,使用 Partition 和 Offset 保证扩展性。
  • RabbitMQ 是传统的消息队列中间件,支持 AMQP 协议,适合轻量级业务异步解耦,原生支持延迟队列和消息确认机制。

我们项目中如果要处理高并发日志或用户行为采集就选 Kafka,如果是下单通知、订单超时等场景就更适合 RabbitMQ。

Kafka中实现高性能的设计有了解过吗?

  • 消息分区:不受单台服务器的限制,可以不受限的处理更多的数据
  • 顺序读写:磁盘顺序读写,提升读写效率
  • 页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问
  • 零拷贝:减少上下文切换及数据拷贝
  • 消息压缩:减少磁盘IO和网络IO
  • 分批发送:将消息打包批量发送,减少网络开销
零拷贝

集合面试篇

算法复杂度分析

什么是算法时间复杂度?

  • 时间复杂度表示了算法的执行时间数据规模之间的增长关系

常见的时间复杂度有哪些?口诀:常对幂指阶

  • O(1)、O(n)、O(n^2)、O(logn)

什么是算法的空间复杂度?

  • 表示算法占用的额外存储空间数据规模之间的增长关系
    常见的空间复杂度:O(1)、O(n)、O(n^2)
为什么要进行复杂度分析?
  • 指导你编写出性能更优的代码
  • 评判别人写的代码的好坏
时间复杂度分析:来评估代码的执行耗时的
  • 大O表示法:不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势

  • 只要代码的执行时间**不随着n的增大而增大,这样的代码复杂度都是O(1)**

  • 复杂度分析就是要弄清楚代码的执行次数数据规模n之间的关系

时间复杂度:全称是渐进空间复杂度,表示算法占用的额外存储空间数据规模之间的增长关系

List相关面试题

  • 数组是一种用连续的内存空间存储相同数据类型数组的线性数据结构

  • 数组下标为什么从0开始

    寻址公式是:baseAddress + i * data TypeSize 计算下标的内存地址效率较高

  • 查找的时间复杂度

    • 随机(通过下标)查询的时间复杂度是O(1)
    • 查找元素(未知下标)的时间复杂度是O(n)
    • 查找元素(未知下标但排序)通过二分查找的时间复杂度是O(logn)
  • 插入和删除时间复杂度

    插入和删除的时候,为了保证数组的内存连续性,需要挪动数组元素,平均复杂度为O(n)

底层实现
  • 数据结构—数组
  • ArrayList源码分析
面试问题
  • ArrayList底层的实现原理是什么
  • ArrayList list = new ArrayList(10)中的list扩容几次
  • 如何实现数组和List之间的转换
  • ArrayList和LinkedList的区别是什么

ArrayList源码分析

List< Integer > list = new ArrayList< Integer >();
list.add(1)

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用于空实例的共享空数组实例
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于默认大小的空实例的共享空数组实例
     * 与上面的区分开,以了解添加第一个元素时要膨胀多少
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  
    /* 存储ArrayList元素的数组缓冲区,ArrayList的容量就是这个数组缓冲区的长度 */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList的大小(包含的元素数量)
     * @serial
     */
    private int size;
...
}

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

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
       // 创建一个真正存储集合位置的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
    // 如果容量是0则创建一个新的数组给elementData
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    public ArrayList() { 
         // 无参构造函数,默认创建空集合
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

-------------------------------------------------------------------------------
// Collection是所有单列集合的父接口
// 将 Collection 对象转换成数组,然后将数组的地址赋给 elementData
 public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // 判断集合类型是否为不为object[] 
            // 在其他jdk此处是 ?? == ArrayList.class
            if (elementData.getClass() != Object[].class)
                // 不是的话就拷贝到数组elementData中
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
ArrayList源码分析-添加和扩容操作(第1次添加数据)

ArrayList底层的实现原理是什么

  • ArrayList底层是用动态数组实现的
  • ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
  • ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
  • ArrayList在添加数据的时候
    • 确保数组已使用长度(size)加1之后足够存下下一个数据
    • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
    • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上
    • 返回添加成功布尔值

ArrayList list = new ArrayList(10)中的list扩容几次

  • 该语句只是声明和实例了一个ArrayList,指定了容量为10,未扩容

如何实现数组和List之间的转换

  • 数组转List,使用JDK中java.util.Arrays工具类的asList方法
  • List转数组,使用List的toArray方法,无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组

使用 Hutool 工具库可以非常方便地实现数组和 List 之间的转换。Hutool 提供了 ArrayUtilCollUtil 工具类来处理数组和集合之间的转换。

问:我不能用BeanUtil吗?
答:BeanUtil 是 Hutool 工具库中用于 Java Bean 操作的工具类,主要用于 对象属性拷贝Bean 转 MapMap 转 Bean 等操作。它并不适用于 数组和 List 之间的转换

如果你误以为 BeanUtil 可以用于数组和 List 的转换,可能是因为它的名字容易让人误解。实际上,数组和 List 的转换应该使用 ArrayUtilCollUtil

正确的工具类选择

  1. 数组转 List:使用 ArrayUtil.toList
  2. List 转数组:使用 CollUtil.toArray
  3. Bean 属性拷贝:使用 BeanUtil.copyProperties
  4. Bean 转 Map:使用 BeanUtil.beanToMap
  5. Map 转 Bean:使用 BeanUtil.fillBeanWithMap
更多的Hutool工具使用高能预警
1. 字符串工具类:StrUtil
  • 功能:字符串操作,如判空、格式化、截取、替换等。
  • 常用方法
    • StrUtil.isEmpty():判断字符串是否为空。
    • StrUtil.format():格式化字符串。
    • StrUtil.split():拆分字符串。
    • StrUtil.join():连接字符串。

2. 日期时间工具类:DateUtil
  • 功能:日期和时间的格式化、解析、计算等。
  • 常用方法
    • DateUtil.now():获取当前时间。
    • DateUtil.format():格式化日期。
    • DateUtil.parse():解析字符串为日期。
    • DateUtil.offsetDay():日期加减。

3. 文件工具类:FileUtil
  • 功能:文件和目录的操作,如读写、复制、删除等。
  • 常用方法
    • FileUtil.readUtf8String():读取文件内容为字符串。
    • FileUtil.writeUtf8String():将字符串写入文件。
    • FileUtil.copy():复制文件或目录。
    • FileUtil.del():删除文件或目录。

4. JSON 工具类:JSONUtil
  • 功能:JSON 的解析和生成。
  • 常用方法
    • JSONUtil.parseObj():将 JSON 字符串解析为 JSON 对象。
    • JSONUtil.parseArray():将 JSON 字符串解析为 JSON 数组。
    • JSONUtil.toJsonStr():将对象转换为 JSON 字符串。

5. 集合工具类:CollUtil
  • 功能:集合操作,如创建集合、判空、过滤、分组等。
  • 常用方法
    • CollUtil.newArrayList():快速创建 ArrayList。
    • CollUtil.isEmpty():判断集合是否为空。
    • CollUtil.filter():过滤集合。
    • CollUtil.group():对集合进行分组。

6. 反射工具类:ReflectUtil
  • 功能:反射操作,如调用方法、获取字段、创建对象等。
  • 常用方法
    • ReflectUtil.invoke():调用方法。
    • ReflectUtil.getFieldValue():获取字段值。
    • ReflectUtil.newInstance():创建对象实例。

7. HTTP 工具类:HttpUtil
  • 功能:HTTP 请求的发送和响应处理。
  • 常用方法
    • HttpUtil.get():发送 GET 请求。
    • HttpUtil.post():发送 POST 请求。
    • HttpUtil.downloadFile():下载文件。

8. 加密解密工具类:SecureUtil
  • 功能:常见的加密解密操作,如 MD5、SHA、AES 等。
  • 常用方法
    • SecureUtil.md5():计算 MD5 值。
    • SecureUtil.sha256():计算 SHA-256 值。
    • SecureUtil.aes():AES 加密解密。

9. IO 工具类:IoUtil
  • 功能:IO 流操作,如读写、关闭流等。
  • 常用方法
    • IoUtil.read():读取流内容。
    • IoUtil.write():写入流内容。
    • IoUtil.close():关闭流。

10. 随机工具类:RandomUtil
  • 功能:生成随机数、随机字符串等。
  • 常用方法
    • RandomUtil.randomInt():生成随机整数。
    • RandomUtil.randomString():生成随机字符串。
    • RandomUtil.randomEle():从集合中随机选择一个元素。

11. 验证工具类:Validator
  • 功能:数据验证,如邮箱、手机号、身份证等。
  • 常用方法
    • Validator.isEmail():验证是否为邮箱。
    • Validator.isMobile():验证是否为手机号。
    • Validator.isCitizenId():验证是否为身份证号。

12. 缓存工具类:CacheUtil
  • 功能:简单的缓存操作。
  • 常用方法
    • CacheUtil.newTimedCache():创建定时缓存。
    • CacheUtil.put():添加缓存。
    • CacheUtil.get():获取缓存。

13. 线程工具类:ThreadUtil
  • 功能:线程操作,如睡眠、创建线程池等。
  • 常用方法
    • ThreadUtil.sleep():线程睡眠。
    • ThreadUtil.newExecutor():创建线程池。

14. Excel 工具类:ExcelUtil
  • 功能:Excel 文件的读写操作。
  • 常用方法
    • ExcelUtil.getReader():读取 Excel 文件。
    • ExcelUtil.getWriter():写入 Excel 文件。

15. 压缩工具类:ZipUtil
  • 功能:文件或目录的压缩和解压缩。
  • 常用方法
    • ZipUtil.zip():压缩文件或目录。
    • ZipUtil.unzip():解压缩文件。

16. 日志工具类:Log
  • 功能:简化日志操作。
  • 常用方法
    • Log.get():获取日志对象。
    • Log.info():输出日志信息。

17. 数学工具类:MathUtil
  • 功能:数学计算,如四舍五入、最大值、最小值等。
  • 常用方法
    • MathUtil.round():四舍五入。
    • MathUtil.max():获取最大值。
    • MathUtil.min():获取最小值。

18. 网络工具类:NetUtil
  • 功能:网络相关操作,如获取本机 IP、Ping 等。
  • 常用方法
    • NetUtil.getLocalhost():获取本机 IP。
    • NetUtil.ping():Ping 测试。
  • Arrays.asList转List后,如果修改了数组内容,list受影响吗
  • List用toArray转数组后,如果修改了List内容,数组受影响吗
再答:
  • Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
  • list用了toArray转数组后,如果修改了list内容,数组不会受影响,当调用了toArray以后,在底层是它进行了数组拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响

LinkedList的数据结构—链表

单向链表

  • 链表中的每一个元素称之为结点(Node)
  • 物理存储单元上,非连续、非顺序的存储结构
  • 单向链表:每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。记录下个结点地址的指针叫作后继指针 next
1.单向链表和双向链表的区别是什么
  • 单向链表只有一个方向,结点只有一个后继指针 next。
  • 双向链表它支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点
2.链表操作数据的时间复杂度是多少
查询 新增删除
单向链表 头O(1), 其他O(n) 头O(1), 其他O(n)
双向链表 头尾O(1), 其他O(n), 给定节点O(1) 头尾O(1), 其他O(n), 给定节点O(1)

ArrayList和LinkedList的区别

  • 底层数据结构
    • ArrayList 是动态数组的数据结构实现
    • LinkedList 是双向链表的数据结构实现
  • 操作数组效率

    • ArrayList 按照下标查询的时间复杂度O(1);【内存是连续的,根据寻址公式】,LinkedList不支持下标查询

    • 查找(未知索引):ArrayList需要遍历,链表也需要遍历,时间复杂度都是O(n)

    • 新增删除【查询多用ArrayList;插入/删除多用Linked

      • ArrayList 尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
      • LinkedList 头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
  • 内存空间占用

    • ArrayList 底层是数组,内存连续,节省内存
    • LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
  • 线程安全

    • ArrayList和LinkedList都不是线程安全的

    • 如果要保证线程安全,有两种方法

      • 方法内使用,局部变量则是线程安全的

      • 使用线程安全的ArrayList和LinkedList

        List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>());
        List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());
        

HashMap相关面试题

  • 二叉树

    • 满二叉树

    • 完全二叉树

    • 二叉搜索树

      二叉搜索树又名二叉查找树,有序二叉树或者排序二叉树,是二叉树中比较常用的一种类型二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值

    • 红黑树

  • 红黑树

  • 散列表

✅ 1. 二叉树(Binary Tree)

每个节点最多有两个子节点,称为左子节点和右子节点。


✅ 2. 满二叉树(Full Binary Tree)

每个节点要么是叶子节点,要么恰好有两个子节点,且所有叶子都在同一层


✅ 3. 完全二叉树(Complete Binary Tree)

除了最后一层,其他每一层的节点数都达到最大值,且最后一层节点集中在左侧


✅ 4. 二叉搜索树(Binary Search Tree,BST)

又叫:二叉查找树、有序二叉树

特点:

  • 对于任意一个节点 node
    • 左子树中所有节点值 < node
    • 右子树中所有节点值 > node
  • 中序遍历是升序排列

用途:

  • 用于快速查找、插入、删除(时间复杂度平均为 O(log n),最坏为 O(n))

✅ 5. 红黑树(Red-Black Tree)

红黑树是自平衡的二叉搜索树,在多种语言的底层集合结构(如 Java 的 TreeMapTreeSet,C++ 的 mapset)都有使用。

特点:

  • 每个节点是红或黑
  • 根节点是黑色
  • 每个叶子节点(NIL)是黑色
  • 红色节点不能有红色子节点(即不能连续两个红)
  • 任意一节点到其所有后代叶子节点的路径上,黑色节点数量相同

目的:

  • 保证在最坏情况下,查找、插入、删除的时间复杂度是 O(log n)

✅ 6. 散列表(Hash Table)

与树不同,散列表是通过哈希函数(Hash Function)将键映射到数组下标进行查找。

特点:

  • 查找时间接近 O(1)
  • 冲突处理方式如链地址法、开放定址法等
  • 用于实现如 Java 的 HashMapHashSet、Python 的 dictset 等结构

数据结构—红黑树 什么是红黑树?

  • 红黑树:也是一种自平衡的二叉搜索树(BST)
  • 所有的红黑规则都是希望红黑树能够保证平衡
  • 红黑树的时间复杂度:查找、添加、删除都是O(logn)

散列表

什么是散列表?

  • 散列表(Hash Table)又叫哈希表/Hash表
  • 根据键(Key)直接访问再内存存储位置值(Value)的数据结构
  • 由数组演化而来的,利用了数组支持按照下标进行随机访问数据

散列冲突

  • 散列冲突又成为哈希冲突,哈希碰撞
  • 指多个key映射到同一个数组下标位置

散列冲突—链表法(拉链)

  • 数组的每个下标位置称之为(bucket) 或者 (slot)
  • 每个桶(槽)会对应一条链表
  • hash冲突后的元素都放到相同槽位对应的链表中或红黑树中

HashMap中的最重要的一个数据结构就是散列表,在散列表中又用到了红黑树链表
散列表(Hash Table)又名为哈希表/Hash表,是根据键(Key)直接访问在内存存储位置值(value)的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性[根据寻址公式,时间复杂度O(1)]

说一下HashMap的实现原理

1.说一下HashMap的实现原理

  • HashMap的数据结构:底层使用hash表数据结构,即数组和链表或红黑树

  • 添加数据时,计算key的值确定元素在数组中的下标

    • key相同则替换
    • 不同则存入链表或红黑树中

    获取数据通过key的hash计算数组下标获取元素

2.HashMap的jdk1.7和jdk1.8有什么区别

  • JDK1.8之前采用的拉链法,数组+链表
  • JDK1.8之后采用数组+链表+红黑树
    链表长度大于8且数组长度大于64则会从链表转化为红黑树

当我们往HashMap中put元素时(扰动函数),利用key的hashCode重新hash计算出当前对象的元素在数组中的下标

HashMap的put方法的具体流程

1.判断键值对数组table是否为空或为null,否则执行resize()进行扩容 [初始化]
2.根据键值key计算hash值得到数组索引
3.判断table[i] == null,条件成立,直接新建节点添加
4.如果table[i] == null,不成立
4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
4.2 判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对
4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value

讲一下HashMap的扩容机制

HashMap源码分析

桶下标是hash值取模数组(长度)下标 capacity

HashMap的寻址算法

Hash值右移16位后与原来的hash值进行异或运算【扰动算法hash值更加均匀,减少hash冲突
数组长度必须是2的n次幂 按位与运算的效果才能代替取模

int hash = h ^ (h >>> 16); // hashCode 的扰动处理
index = hash & (table.length - 1); // 更快的取模运算

HashMap在1.7情况下的多线程死循环问题

jdk7的数据结构是:数组+链表
在数组进行扩容的时候,因为链表是头插法(在并发情况下可能出现链表反转成环形结构),在进行数据迁移的过程中,有可能导致死循环

进程和线程的区别?

两者对比:
  • 进程是整个在运行程序的实例进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
类型 简要定义
进程 程序的执行实例,资源分配的最小单位
线程 进程内的执行单元,cpu调度的最小单位
项目 进程 线程
概念 正在运行的程序实例 进程中的执行单元
所属关系 进程可包含多个线程 线程依附于进程存在
内存空间 拥有独立地址空间 共享所属进程的内存空间
创建开销 创建/销毁成本高(需要资源分配) 创建/销毁成本低(共享资源)
通信方式 进程间通信较复杂(如管道/套接字) 线程通信简单(共享变量)
崩溃影响 一个进程崩溃不会影响其他进程 一个线程崩溃可能影响整个进程
切换开销 上下文切换开销大 上下文切换开销小
资源隔离 资源独立,安全性高 资源共享,效率高但易出错
[ 进程A ]
 ├── 线程1:负责计算
 ├── 线程2:负责文件IO
 └── 线程3:负责网络通信

[ 进程B ]
 └── 线程1:完全独立

程序由指令数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个**进程**

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。一个进程之内可以分为一到多个线程

core → 线程1[指令1,指令2,指令3…] 线程2[指令1,指令2,指令3…]

并行和并发的区别?

现在都是多核CPU,在多核CPU下

  • 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
  • 并行是同一时间动手做多件事的能力,4核CPU同时执行4个线程
概念 定义
并发 同一时间段处理多个任务的能力(任务轮流切片执行)
并行 同一时刻真正同时执行多个任务(多个核同时执行)

==单核CPU== → 单核CPU下线程实际还是串行执行的

  • 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15ms)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的
  • 每个时间片只能用有一个线程被执行
  • 总结一句话:**微观串行,宏观并行**
  • 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
CPU 时间片1 时间片2 时间片3
core 线程1 线程2 线程3

==多核CPU== → 每个核(core)都可以调度运行线程,这个时候线程是可以并行的

CPU 时间片1 时间片2 时间片3 时间片4
core1 线程1 线程2 线程3 线程3
core2 线程2 线程4 线程2 线程4

并发 (concurrent) 是同一时间应对 (dealing with) 多件事情的能力
并行 (parallel) 是同一时间动手做 (doing) 多件事情的能力

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这么多件事,这就是并发【单核CPU】
  • 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行【会产生竞争,例如锅只有一个,一个人用锅时,另一个人就要等待】
  • 雇了3个保姆,一个专门做饭,一个专门打扫卫生,一个专门喂奶,互不干扰,这就是并行

创建线程的方式有哪些?

  • 继承Thread类,重写run方法
public class MyThread extends Thread{
    @Override
    public void run(){
        sout("MyThread...run...");
    }
    public static void main(String[] args){
        // 创建MyThread对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        
        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}
  • 实现Runnable接口,重写run方法
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        // 在这里编写要执行的任务
        System.out.println("线程正在执行任务...");
    }
    public static void main(String[] args) {
        // 创建MyRunnable实例
        MyRunnable myRunnable = new MyRunnable();
        
        // 创建线程并启动
        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);
        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}
  • 实现Callable< T >接口,重写call方法泛型和重写方法一致
public class MyCallable implements Callable<String> {
 @Override
    public String call() throws Exception {
        sout(Thread.currentThread().getName());
        return "ok";
    }
     public static void main(String[] args) {
        // 创建MyCallable实例
        MyCallable myCallable = new MyCallable();
        
        // 使用FutureTask来包装Callable对象
        FutureTask<String> ft = new FutureTask<String>(myCallable);
        
        // 创建并启动线程
        Thread t1 = new Thread(ft);
        t1.start();
        // 调用ft的get方法获取执行结果
        String result = ft.get();
        sout(result)
    }
}
  • 线程池创建线程 (项目中使用的方式)
public class MyExecutors implements Runnable{
    @Override
    public void run(){
        sout("MyRunnable...run...");
    }
    public static void main(String[] args){
        // 创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors()); 
        //submit用来提交线程
        
        // 关闭线程池
        threadPool.shutdown();
    }
}
刚刚你说过,使用runnable和callable都可以创建线程,它们有什么区别呢?
  • Runnable接口run方法没有返回值
  • Callable接口call方法有返回值,要结合FutureTask配合可以用来获取异步执行的结果

FutureTaskFuture 的实现类,它可以包装一个 CallableRunnable 对象,并允许我们在任务执行完毕后获取执行结果或取消任务。

FutureTask 可以在子线程中异步执行任务,而主线程可以通过调用 FutureTask.get() 方法获取任务执行的结果。

  • Callable接口的call()方法允许抛出异常;而Runnabble接口的run()方法的异常只能在内部消化,不能继续上抛
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableExample {

    public static void main(String[] args) throws Exception {
        // 创建一个Callable任务
        Callable<Integer> task = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("Task is running in the background...");
                // 模拟耗时操作
                Thread.sleep(2000);
                return 42; // 返回计算结果
            }
        };

        // 创建FutureTask对象,包装Callable任务
        FutureTask<Integer> futureTask = new FutureTask<>(task);

        // 启动线程执行FutureTask
        Thread thread = new Thread(futureTask);
        thread.start();

        // 主线程可以做一些其他工作
        System.out.println("Main thread is doing something else...");

        // 获取异步执行结果,阻塞直到任务完成
        Integer result = futureTask.get(); // 这会阻塞主线程直到获取到结果
        System.out.println("Task result: " + result); // 打印任务执行结果
    }
}

在启动线程的时候,可以使用run方法吗?run()和start()有什么区别?

start()是开启一个线程 run()跟开启普通方法一样

start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次
run():封装了要被线程执行的代码,可以被调用多次

线程包括哪些状态,状态之间是如何变化的?

状态:
新建New、可运行Runnable、阻塞Blocked、等待Waiting、时间等待Timed_waiting、终止Terminated

线程状态之间如何变化:

  • 创建线程对象是新建状态
  • 调用了start()方法转变为可执行状态
  • 线程获取到了CPU的执行权,执行结束是终止状态
  • 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
    • 如果没有获取锁(synchronized或lock) 进入阻塞状态,获得锁再切换为可执行状态
    • 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可转换为可执行状态
    • 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
Thread.java
public enum State {
    /**
     * 新建状态。线程已经被创建,但尚未启动。
     */
    NEW,

    /**
     * 可运行状态。线程在JVM中是可运行的,这并不意味着它一定在运行,它可能在等待其他线程或操作系统的资源。
     */
    RUNNABLE,

    /**
     * 阻塞状态。线程正在等待监视器锁,以进入一个同步块/方法,或者在调用Object.wait后等待重新进入同步块/方法。
     */
    BLOCKED,

    /**
     * 等待状态。线程在等待另一个线程执行特定操作。例如,一个线程调用了Thread.join,它在等待指定的线程终止。
     */
    WAITING,

    /**
     * 超时等待状态。线程在等待另一个线程执行特定操作,但它设置了超时时间。如果线程在指定时间内没有等待到所需条件,它将自动返回。
     */
    TIMED_WAITING,

    /**
     * 终止状态。线程已经完成了执行。
     */
    TERMINATED;
}

新建T1、T2、T3三个线程,如何保证它们按顺序执行?

可以使用线程中的join方法解决
join() 等待线程运行结束

t.join() 阻塞调用此方法的线程进入timed_waiting 直到线程t执行完毕后,此线程再继续执行
Thread t1 = new Thread(()->{
    sout("t1");
});
Thread t2 = new Thread(()->{
    try{
        t1.join();
    }catch(InterruptedException e){
        e.printStackTrance();
    }
    sout("t2");
})
Thread t3 = new Thread(()->{
    try{
        t2.join();
    }catch(InterruptedException e){
        e.printStackTrance();
    }
    sout("t3");
});
// 启动线程
t1.start();
t2.start();
t3.start();

notify() 和 notifyAll() 有什么区别?

  • notifyAll:唤醒所有wait的线程
  • notify:只随机唤醒一个wait线程

java中wait和sleep方法有什么区别?wait要和synchronized一起使用

方法 归属类 是否释放锁 唤醒方式 使用前提
sleep() Thread ❌ 不释放锁 时间到、被打断 直接调用即可
wait() Object ✅ 释放锁 notify()/时间到/被打断 必须配合 synchronized 使用
共同点

wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态

不同点
  • 方法归属不同
    • sleep(long)是Thread的静态方法
    • 而wait(),wait(long)都是Object的成员方法,每个对象都有
  • 醒来时机不同
    • 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
    • wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去wait要和synchronized一起使用
    • 它们都可以被打断唤醒
  • 锁特性不同【重点】
    • wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
    • wait方法执行后会释放锁对象,允许其他线程获得该锁对象 (我放弃cpu,但你们还可以用)
    • 而sleep如果在synchronized代码块中执行,并不会释放锁对象 (我放弃cpu,你们也用不了)

✅ 核心区别(面试高频)

维度 sleep() wait()
所属类 Thread 静态方法 Object 实例方法
是否释放锁 ❌ 不释放锁 ✅ 释放当前对象锁
是否需要锁 ❌ 不需要任何锁 ✅ 必须持有该对象的锁(synchronized
唤醒方式 到时间/中断 到时间/中断/notify / notifyAll
使用目的 让线程暂停执行,但持有锁不让别人进 让线程等待并释放锁,协调多线程通信

✅ 场景对比

场景 使用方法 原因
想暂停线程几秒钟(不释放锁) Thread.sleep(ms) 常用于模拟网络延迟/定时任务
多线程协作(生产者-消费者模型) wait() / notify() 用于线程间通信,让出锁资源

✅ 通俗类比 🌰

  • sleep():你在厕所里睡觉,门上锁了(别人不能进),你虽然休息了,但别人也进不来。
  • wait():你说“我出去抽根烟(释放锁)”,别人可以进去用厕所(释放资源),抽完烟再回来(被唤醒)继续工作。

如何停止一个正在运行的线程?

有三种方式可以停止线程
  • 使用退出标志位,使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程
    • 打断阻塞的线程(sleep, wait, join)的线程,线程会抛出InterruptedException异常
    • 打断正常的线程,可以根据打断状态来标记是否退出线程

synchronized关键字的底层原理?底层:Monitor

  • synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
  • 它的底层由monitor实现的,monitor**是jvm级别的现象(C++实现)**,线程获得锁需要使用对象(锁)关联monitor
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset
    • owner是关联的获得锁的线程,并且只能关联一个线程;
    • entrylist关联的是处于阻塞状态的线程;
    • waitset关联的是处于Waiting状态的线程;

synchronized关键字的底层原理—进阶

Monitor实现的锁属于重量级锁,你了解过锁升级吗?

一旦锁发生了竞争,都会升级为重量级锁

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

描述
重量级锁 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低 【有多个线程来抢】
轻量级锁 线程加锁的时间是错开的(也就是没有竞争)可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
  • Monitor实现的锁属于重量级锁,里面涉及到了用户态权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低
  • 在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下使用传统锁机制带来的性能开销问题

每一个 Java 对象在 JVM 中都有一个对象头,其中包含 MarkWord,用于存储锁信息。
当线程访问 synchronized 方法或代码块时,会尝试获取对象关联的 Monitor,进入临界区:Monitor 中包含:

字段 作用说明
owner 当前持有锁的线程
entryList 等待获取锁(阻塞)的线程队列
waitSet 调用 wait() 被挂起的线程队列

获取锁流程(简化):

  1. 检查对象头中的 MarkWord;
  2. 如果未被锁,尝试通过 CAS 设置为当前线程(偏向或轻量级);
  3. 如果竞争失败 → 升级为重量级锁(Monitor);
  4. 等待唤醒或抢占锁。
锁升级过程(从偏向锁 → 轻量级锁 → 重量级锁)
锁类型 触发条件 优点 场景
偏向锁 只有一个线程访问(无竞争) 几乎无开销,不用 CAS 单线程长时间持有的锁
轻量级锁 多线程访问,但加锁时间错开 使用 CAS,无阻塞,性能较高 少量线程短时间交替访问
重量级锁 多线程同时竞争同一把锁 线程阻塞 + 唤醒,开销大 并发激烈,必须互斥的场景
偏向锁 / 轻量级锁 / 重量级锁细节图解(简述)

🚀 偏向锁原理:

  • 第一次访问:CAS 记录当前线程 ID 到对象头
  • 再次访问时:只判断对象头的线程 ID 是否是自己
  • 如果有竞争,偏向锁就会被撤销,升级为轻量级锁

🚀 轻量级锁原理:

  • 线程栈中创建 LockRecord
  • 尝试 CAS 将 LockRecord 指针复制到对象头;
  • 成功 → 获取锁;失败 → 说明竞争,升级为重量级锁

🚀 重量级锁原理(Monitor):

  • Monitor 的实现是基于 操作系统的互斥量(mutex)
  • 涉及线程的挂起、唤醒(用户态 → 内核态切换,开销大)
Monitor重量级锁

每个Java对象都可以关联一个Monitor对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

加锁流程
  • 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
  • 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
  • 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为nul,起到了一个重入计数器的作用。
  • 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
  • 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record.
  • 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
  • 如果Lock Record的 Mark Word不为nul,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
偏向锁性能比轻量级锁好
  • 轻量级锁在没有竞争时(就自己这个线程)每次重入仍然需要执行 CAS 操作。
  • Java6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

你谈谈JMM (Java内存模型)

Java内存模型
  • JMM(Java Memory Model)Java内存模型,定义了共享内存多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
  • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  • 线程跟线程之间是相互隔离,线程跟线程相互需要通过主内存

CAS你知道吗?乐观锁

  • CAS全称是:Compare And Swap(比较再交换),它体现的一种无锁(乐观锁)的思想,在无锁情况下保证线程操作共享数据的原子性。
  • CAS使用到的地方很多:AQS框架、AtomicXXX类
  • 在操作共享变量的时候使用自旋锁,效率上更高一些
  • CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现

比较内存值是否与预期值相等,如果相等则更新为新值;否则不做操作,重新尝试(一般配合自旋)

// 伪代码
if (value == expectedValue) {
  value = newValue;
}

💡 应用场景:

  • java.util.concurrent.atomic 包下的 AtomicInteger
  • ReentrantLock 的底层 AQS
  • ConcurrentHashMap 局部并发控制

在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作

  • AbstractQueuedSynchronizer (AQS框架)
  • AtomicXXX类

乐观锁和悲观锁的区别?

乐观锁 vs 悲观锁(对比表)

特性 乐观锁(CAS) 悲观锁(synchronized/Lock)
思想 默认不会冲突,失败后重试 默认可能冲突,先加锁
开销 CPU 开销高(自旋) 上下文切换成本高
性能 高并发下优于悲观锁 并发低时更稳定
实现方式 CAS,自旋锁,版本号控制 synchronized, ReentrantLock等

谈一谈你对volatile的理解?轻量级的同步机制

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证线程间的可见性

    用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

    当一个线程修改了被 volatile 修饰的变量,新值会立即同步到主内存中,其他线程读取这个变量时也会立即从主内存中刷新,而不是使用线程工作内存中的旧副本。

    • 保证多个线程看到的是同一个值
    • ❌ 但是 不能保证原子性
  • 禁止进行指令重排序

    用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

    • Java 编译器 & CPU 为了提高性能可能会对指令进行优化,造成代码执行顺序书写顺序不一致。

    • volatile 能在变量读写操作前插入内存屏障(Memory Barrier):

      • 写屏障:防止写操作后面的指令重排到前面

      • 读屏障:防止读操作前面的指令被排到后面

    JMM 是理论模型,volatile 是其具体体现。

    🔄 volatile 是 JMM(Java内存模型)在语义上的一个重要实现。

    JMM 内容 volatile 实现作用
    主内存 & 工作内存 强制将值立即刷新到主内存
    可见性保证 ✅ volatile 提供
    原子性保证 ❌ volatile 不提供
    禁止指令重排 ✅ volatile 提供(内存屏障)
    这几个进行一下比较
    特性 volatile synchronized / Lock CAS (AtomicXXX)
    可见性
    原子性 ✅(通过硬件指令)
    重排序控制 ✅(内存屏障) ✅(通过锁的语义) ✅(内存屏障)
    是否加锁 否,轻量,性能高 是,重量级,性能相对低 否,自旋CAS
    适用场景 状态标志、单例双检锁等 临界区互斥、大块同步场景 高并发下原子计数/计量等操作

什么是AQS?

  • 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
  • AQS内部维护了一个**先进先出的双向队列**,队列中存储的排队的线程
  • 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无所状态),如果队列中有一个线程修改成功了state为1,则当前线程就相当于获取了资源。
  • 在对state修改的时候使用CAS(compare and swap)操作,保证多个线程修改的情况下原子性

AQS(AbstractQueuedSynchronizer),即抽象队列同步器。它是构建锁或者其他同步组件的基础框架

AQS与Synchronized的区别
AQS synchronized
java语言实现 关键字,C++语言实现
悲观锁,手动开启和关闭 悲观锁,自动释放锁
锁竞争激烈的情况下,提供了多种解决方案 锁竞争激励都会升级为重量级锁,性能差

CAS和AQS的区别

CAS(Compare And Swap)乐观锁:一种无锁的原子操作机制,用于实现数据层面的原子性,是底层原语。

AQS(AbstractQueuedSynchronizer)悲观锁:一种同步器框架,用于构建锁和同步器(如 ReentrantLock、Semaphore 等),是结构设计。

表格对比:CAS vs AQS

对比点 CAS AQS
全称 Compare And Swap AbstractQueuedSynchronizer
概念类型 原子操作机制(CPU指令级别) 同步器框架(Java并发包核心)
功能目的 保证共享变量原子性更新 实现线程同步控制(排队、阻塞、唤醒等)
属于哪一层 底层原子操作 高层并发框架
是否加锁 否(无锁) 是(加锁或排队等待)
底层依赖 CPU的 CAS 指令(如 cmpxchg CAS、LockSupport、队列、模板方法
应用场景 AtomicXXX、线程安全计数器、乐观锁等 ReentrantLock、Semaphore、CountDownLatch、FutureTask 等
是否自带阻塞/唤醒 ❌ 不具备阻塞机制 ✅ 自带阻塞/唤醒机制(如 condition.await/signal)
实现原理 通过比较内存值 + 原子更新 模板方法 + 状态位 state + FIFO 等待队列
失败机制 自旋重试(乐观锁) 阻塞挂起,进入等待队列

各自常见应用场景

场景 用的是谁? 举例
实现原子操作 CAS AtomicInteger.incrementAndGet()
实现线程排队获取锁 AQS ReentrantLock.lock()
信号量控制 AQS Semaphore.acquire()
倒计时器 AQS CountDownLatch.await()
实现 Future 机制 AQS FutureTask.run()
高并发无锁计数器 CAS LongAdder.add()(改进版

CAS 是 AQS 的底层基础之一:AQS 内部更新同步状态(state)时就用的是 CAS。
AQS 是基于 CAS + FIFO 队列实现的线程同步框架,比 CAS 更复杂、能力更强。

// AQS 内部设置状态的关键方法
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update); // 使用CAS
}

举个例子:ReentrantLock

lock.lock();
  • 内部结构:
    • 使用 AQS 实现公平/非公平锁的排队机制
    • 使用 CAS 来设置 state = 1(加锁)

CAS 是一种基于硬件的原子操作指令,用于在无锁环境下保证共享变量的线程安全,常用于 AtomicInteger 等类。而 AQS 是 Java 并发包中用于构建同步器(如锁、信号量等)的框架,它通过 CAS 操作来维护内部状态 state,并通过一个基于 FIFO 的等待队列来实现线程的阻塞与唤醒。因此,CAS 是底层原语,而 AQS 是上层的并发框架,AQS 内部正是基于 CAS 实现的

AQS常见的实现类
  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

ReentrantLock [rɪ’entrənt]lock 的实现原理?[关联HashMap线程不安全需加锁(synchronized或ReentrantLock)]

ReentrantLock主要利用CAS+AQS队列CompareAndSwap+AbstractQueuedSynchronized来实现。**它支持公平锁和非公平锁,两者的实现类似构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁**。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断synchronized不可中断
  • 可设置超时时间没有获得锁时只能进入等待[没有获取锁可以放弃锁]
  • 可以设置公平锁synchronized只有非公平锁[也支持非公平锁]
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

synchronized和Lock有什么区别?

  • 语法层面

synchronized是关键字,源码在jvm中,用c++语言实现
Lock是接口,源码由jdk提供,用
java
语言实现
使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁

  • 功能层面

二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock有适合不同场景的实现,如ReentrantLock、ReentrantReadWeiteLock(读写锁)

死锁产生的条件是什么

死锁:一个线程需要同时获取多把锁,这时就容易发生死锁

如何进行死锁诊断 ?

当程序出现了死锁现象,我们可以使用jdk自带的工具:jpsjstack

  • jps:输出JVM中运行的进程状态信息
  • jstack:查看java进程内线程的堆栈信息

JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁

你可以使用jstack命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态

  1. 首先,找到你的Java进程ID(PID)。你可以使用jps命令来列出所有正在运行的Java进程及其PID。

    jps
    
  2. 使用jstack命令打印出该Java进程的堆栈跟踪。

    jstack -l <PID>
    

    <PID>替换为实际的进程ID。

  3. 查找堆栈跟踪中的”DEADLOCK”关键字。jstack会自动检测死锁并在输出中报告。

其他解决工具,可视化工具
  • jconsole

用于对jvm的 内存,线程,类 的监控,是一个基于jmx的GUI性能监控工具
打开方式:java安装目录 bin目录下 直接启动 jconsole.exe就行

  • VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的cpu时间和内存中的对象,已被GC的对象,反向查看分配的堆栈
打开方式:java安装目录 bin目录下 直接启动 jvisualvm.exe就行

死锁:两个线程争夺两个资源的时候 1线程拿到a 想拿b 2线程拿到了b 想拿a
四个原因互斥条件 请求保持 不可剥夺 循环等待
产生死锁的四个因素 同时满足才会死锁 想要解决死锁 需要打破其中一个原因就行

  1. 互斥条件(Mutual Exclusion):资源不能被多个线程同时使用。即某个资源在一段时间内只能由一个线程占用,其他线程必须等待该资源被释放后才能使用。
  2. 持有和等待条件(Hold and Wait):线程至少持有一个资源,并且正在等待获取额外的资源,而该资源又被其他线程持有。
  3. 非抢占条件(No Preemption):已经分配给某个线程的资源在该线程完成任务前不能被抢占,即只能由线程自己释放。
  4. 循环等待条件(Circular Wait):存在一种线程资源的循环等待链,每个线程都在等待下一个线程所持有的资源。

在实际操作中,以下是一些打破死锁的具体方法:银行家算法可以避免死锁

  • 资源分配图:使用资源分配图来检测循环等待条件,并在检测到循环时采取措施。
  • 锁排序:确保所有线程以相同的顺序获取锁,从而避免循环等待。
  • 超时机制:线程在请求资源时设置超时时间,如果超过时间未获得资源,则放弃当前任务并释放已持有的资源。
  • 死锁检测算法:运行死锁检测算法,如银行家算法,来检测系统中的死锁,并在必要时采取措施。
  • 线程中断:允许系统或其他线程中断正在等待资源的线程。
  • 回滚操作:如果检测到死锁,可以让某些线程回滚它们的工作,并释放资源,从而打破死锁。

MySQL是不会有死锁的 自身会检测 [让后面的超时释放回滚]
在分布式事务 线程1拿着资源a是数据库1 线程2拿着资源b是数据库2
JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁

你可以使用jstack命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态

  1. 首先,找到你的Java进程ID(PID)。你可以使用jps命令来列出所有正在运行的Java进程及其PID。

    jps
    
  2. 使用jstack命令打印出该Java进程的堆栈跟踪。

    jstack <PID>
    

    <PID>替换为实际的进程ID。

  3. 查找堆栈跟踪中的”DEADLOCK”关键字。jstack会自动检测死锁并在输出中报告。

聊一下ConcurrentHashMap

ConcurrentHashMap是一种线程安全的高效Map集合
底层数据结构

  • JDK1.7底层采用分段的数组+链表实现

  • JDK1.8采用的数数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树

    在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表,采用CAS + Synchronized来保证并发安全进行实现

    • CAS控制数组节点的添加
    • synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题,效率得到提升

加锁的方式

  • JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
  • JDK1.8采用CAS自旋锁添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好

JDK1.7ConcurrentHashMap 实现中:

  • ConcurrentHashMap 底层被分成了多个 Segment(段)。
  • 每个 Segment 本质上就是一个小型的 HashMap + 一把锁(ReentrantLock)。
  • 整个 Map 是由多个 Segment 组成的数组:Segment<K, V>[] segments;
  • 每个 Segment 管理自己那部分的数据,互不干扰,从而实现高并发。

提高并发性,减小锁竞争:

  • 将一个大的 HashMap 拆成多个 Segment(默认16个),每个 Segment 单独加锁。
  • 这样多个线程并发访问不同 Segment 的数据时,就不会互相阻塞,从而提升性能。
  • 线程只会锁定自己需要访问的那个 Segment,不会锁全表。
【下列图中针对于整体和put的解释】

📌 1. 整体结构

  • 外部是一个 Segment 数组:每个 Segment 是独立加锁的。
  • 每个 Segment 内部又是一个 HashEntry 数组(就像 HashMap 的结构)。

📌 2. put 操作流程(以 JDK1.7 为例):

  1. 根据 key 的 hash 计算出 Segment 的下标(如 Segment[5])。
  2. 进入对应的 Segment,获取其锁ReentrantLock.lock())。
  3. 再在该 Segment 中,查找对应的桶位(HashEntry 数组)。
  4. 找到位置后:
    • 如果该位置已有数据,做链表遍历、替换或追加。
    • 如果链表过长,在 JDK1.7 仍然是链表(没有红黑树)。
  5. 插入完成后释放锁。
✅ JDK1.8 为什么放弃 Segment?

JDK1.8 里,取消了 Segment 分段锁结构,改为节点粒度的同步控制

  • 使用 CAS + synchronized 替代了 Segment + ReentrantLock。
  • 好处:
    • 不再有 Segment 的内存占用与操作复杂度。
    • 粒度更细,性能更好。
    • 数据结构与 HashMap 接轨,统一维护。

在 JDK1.7 中,ConcurrentHashMap 使用 Segment 分段锁机制 提高并发性能,将 Map 拆成多个小的 Segment,每个 Segment 内部结构类似 HashMap,通过加锁控制并发。而在 JDK1.8 中,放弃 Segment,采用 CAS + synchronized 锁节点的方式,结构变为数组 + 链表 + 红黑树,性能与简洁性双双提升。

导致并发程序出现问题的根本原因是什么 (Java程序中怎么保证多线程的执行安全)

Java并发编程三大特性
  • 原子性synchronized、lock:一个线程在CPU中操作不可暂停,也不可中断,要么执行完成,要么不执行
int ticketNum = 10;
public void getTicket(){
    if(ticketNum <= 0){
        return;
    }
    sout(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
    // 非原子性操作
    ticketNum--;
}
main{
    TicketDemo demo = new TicketDemo();
    for(int i = 0; i < 20; i++){
        new Thread(demo::getTicket).start();
    }
}
不是原子操作,怎么保证原子操作呢?
  1. synchronized:同步加锁
  2. JUC里面的lock:加锁

  • 可见性volatile、synchronized、lock
内存可见性:让一个线程对共享变量的修改对另一个线程可见
public class VolatileDemo{
    private static boolean flag = false;
    public static void main(String[] args) throws InterruptedException{
        new Thread(()->{
            while(!flag){
                sout("第一个线程执行完毕...");
            }
        }).start();
        Thread.sleep(100);
        new Thread(()->{
            flag = true;
            sout("第二个线程执行完毕...");
        }).start();
    }
}

解决方案:synchronized、volatile、LOCK

volatile:加在共享变量上面即可 → private static volatile boolean flag = false;

  • 有序性volatile

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

int x;
int y;
@Actor
public void actor1(){
    x = 1;
    y = 1;
}
@Actor
public void actor2(II_Result r){
    r.r1 = y;
    r.r2 = x;
}

解决办法:在前面加上volatile

说一下线程池的核心参数

为什么要创建线程池 因为每次创建线程的时候就要占用一定的内存空间 无限创建线程会浪费内存严重会导致内存溢出
CPU有限的同一时刻只能同时处理一个线程 大量线程来的话就没有线程权 会造成线程等待 造成大量线程在之间切换也会导致性能变慢

在这个例子中,我们创建了一个线程池,核心线程数为5,最大线程数为10,如果线程池中的线程数大于核心线程数,则空闲线程在60秒后会被终止。工作队列使用ArrayBlockingQueue,其容量为100。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 核心线程数
        int corePoolSize = 5;
        // 最大线程数 = (核心线程 + 救急线程的最大数目)
        int maximumPoolSize = 10;
        // 线程池中超过 corePoolSize 数量的空闲线程最大存活时间
        long keepAliveTime = 60L;
        // 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
        TimeUnit unit = TimeUnit.SECONDS;
        // 工作队列,用于存放提交的任务 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
        ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
        // 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
        ThreadFactory threadFactory = new ThreadFactory;
        // 拒绝策略 - 当所有线程都繁忙,workQueue也繁忙时,会触发拒绝策略
        RejectedExecutionHandler handler = new RejectedExecutionHandler;
        
        // 创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue
        );

        // 示例:向线程池提交任务  threadPoolExecutor.submit()/.execute()
        for (int i = 0; i < 20; i++) {
            int taskNumber = i;
            threadPoolExecutor.execute(() -> {
                System.out.println("Executing task " + taskNumber);
                // 模拟任务执行时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}

一开始new的时候没有是空的。先当一个任务提交给线程池时,线程池首先检查当前运行的线程数是否达到核心线程数。如果没有达到核心线程数,线程池会创建一个新的线程来执行任务。如果已经达到核心线程数,线程池会将任务放入工作队列中等待执行。如果工作队列满了,并且当前运行的线程数小于最大线程数,线程池会创建新的线程来执行任务。如果工作队列满了,并且当前运行的线程数等于最大线程数,线程池会根据拒绝策略

拒绝策略:
  • 丢弃任务抛出异常
  • 丢弃任务不抛弃异常
  • 丢弃队列最前面的任务,然后重新提交被拒绝的任务、
  • 由主线程处理该任务来处理无法执行的任务。【线程池无法起到异步问题】
    • 问题:想继续异步且不丢弃任务怎么办?
    • 把这个业务先存到别的地方 ↓↓↓
  • 自定义拒绝策略 自己写实现类实现拒绝策略 可以先存到mysql到时候再慢慢搞

线程池中有哪些常见的阻塞队列

线程工厂可以设置创建的属性
守护线程:主线程(main)一天不死 守护线程不死 [同生共死]
非守护线程:new一个就是 [不是同生共死]

workQueue - 阻塞队列常用的队列:当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

  1. ArrayBlockingQueue: 基于数组结构的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。创建时需要指定容量。【底层是数组 随机读写的 **时间复杂度O(1)**】
    • 开辟新空间创建新数组 把旧数组的数据迁移过去 new ArrayList为空 需要add才可以 扩容是+10 取1.5倍
    • 高并发不会超过某个值 数组不会涉及到扩容 性能会好一些【比较稳定能预估】
    • new的时候不用指定长度
  2. LinkedBlockingQueue: 基于链表结构的有界阻塞队列(如果不指定容量,则默认为Integer.MAX_VALUE,即视为无界)。按照先进先出的原则排序元素。【随机读写的 时间复杂度O(n) 随机读写快 查询慢 是通过二分查找定位到下标元素(通过下标访问数组和链表) 只会走一次二分查找】
    • 读中间的慢 读头尾快
    • 新增元素不涉及到数组的迁移
    • 一般情况下高并发推荐使用,因为队列高级数据结构(可以用数组和链表的实现 由于底层数据结构不同)的特性是先进先出,链表不涉及到数组的扩容 末尾的最快是O(1)【不稳定】
    • new的时候可指定长度是最大链表的长度
    • 不可指定长度 [有界队列&无界队列] → 可能产生JVM的OOM
  3. DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中时间最靠前的
  4. SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
ArrayBlockingQueue LinkedBlockingQueue不给值默认最大值
强制有界 默认无界,支持有界
底层是数组 底层是链表
提前初始化Node数组 是懒惰的,创建节点的时候添加数据
Node需要是提前创建好的 入队会生成新Node
一把锁 两把锁(头尾)可以一边入队,一边出队

ArrayBlockingQueue(数组有界队列)

特性 描述
类型 有界队列(必须指定容量)
底层结构 数组(先进先出 FIFO)
线程安全 内部使用一把锁(ReentrantLock)实现
性能 读写快,结构稳定
应用场景 可以准确预估任务数量的场景,推荐用于生产环境保障系统稳定
特点 不支持扩容,满了会阻塞或抛异常

适用于:生产环境中任务量可控,保证内存稳定,不希望触发OOM。

LinkedBlockingQueue(链表无界队列)

特性 描述
类型 默认无界队列(最大为 Integer.MAX_VALUE,可设置为有界)
底层结构 链表(FIFO)
线程安全 使用两把锁(put锁 + take锁),可以同时入队出队
性能 插入删除性能好,随机访问性能差(O(n))
应用场景 任务流量大、不可预估任务量的场景
特点 不容易触发拒绝策略,但容易造成内存溢出(OOM)

适用于:高并发日志、事件处理等消费速度快但生产不确定的情况。

队列类型 有界性 底层结构 锁机制 特点描述
ArrayBlockingQueue 有界 数组 一把锁 性能稳定,适合任务量可控场景
LinkedBlockingQueue 默认无界 链表 两把锁(头尾) 插入删除效率高,容易堆积任务造成内存压力

线程池中选择哪个阻塞队列?

使用场景 推荐队列类型 说明
普通后台异步任务 ArrayBlockingQueue 稳定、安全,可预估任务量
高并发任务,消费快但产量不可控 LinkedBlockingQueue 适合吞吐量大场景,注意内存风险

守护线程补充

你提到的这段也非常好,总结如下:

  • 守护线程(daemon):依附主线程存在,主线程结束,守护线程也自动终止。如:GC线程。
  • 非守护线程(user thread):默认类型,主线程结束后仍会继续运行。

可通过:

Thread thread = new Thread(...);
thread.setDaemon(true); // 设置为守护线程

如何确定核心线程数

① 高并发、任务执行时间短 → (CPU核数 + 1),减少线程上下文的切换
② 并发不高、任务执行时间长

  • IO密集型任务 → (CPU核数 * 2 + 1)
  • 计算密集型任务 → (CPU核数 + 1)

并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置参考②

  • IO密集型任务:文件读写、DB读写、网络请求等 核心线程数大小设置为2N+1
  • CPU密集型任务:计算型代码、Bitmap转换、Gson转换等 核心线程数大小设置为N+1
// 查看机器的CPU核数
public static void main(String[] args){
    // 查看机器的CPU核数
    System.out.println(Runtime.getRuntime().avaliableProcessors());
}

线程池的种类有哪些

java.util.concurrent.Executors类中提供了大量创建线程池的静态方法,常见的有四种

① 创建使用固定线程数的线程池

适用于任务已知,相对耗时的任务

public static ExecutorService newFixedThreadPool(int nThreads){
    return new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS.new LinkedBlockingQueue<Runnable>)
}
  • 核心线程数与最大线程数一样,没有救急线程 = 最大线程数 - 核心线程数
  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
② 单线程化的线程池它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行→[先进先出]

适用于按照顺序执行的任务

public static ExecutorService newSingleThreadExecutor(){
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
  • 核心线程数和最大线程数都是1
  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
③ 可缓存线程池
public static ExecutorService newCachedThreadPool(){
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
  • 核心线程数为0
  • 最大线程数是Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue: 不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
④ 提供了 延迟周期执行 功能的ThreadPoolExecutor
public ScheduledThreadPoolExecutor(int corePoolSize){
    super(corePoolSize, Integer.MAX_VALUE,0,NANOSECONDS,new DelayedWorkQueue());
}

为什么不建议使用Executors创建线程池?

参考阿里开发手册

【强制】 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1. FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2. CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
    
    
在实际开发中,不建议使用 Executors 创建线程池,因为其底层默认参数具有潜在的 OOM 风险。例如 FixedThreadPool 使用无界队列、CachedThreadPool 最大线程数为 Integer.MAX_VALUE,容易在高并发场景下造成内存溢出。因此建议通过 ThreadPoolExecutor 显式指定核心参数,做到资源可控,避免系统风险。

线程池的使用场景①:ES数据批量导入

CountDownLatch

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时时(一个或多个线程,等待其他多个线程完成某件事情之后才能执行)

  • 其中构造参数用来初始化等待计数值
  • await()用来等待计数归零
  • countDown()用来让计数减一
多线程使用场景一 (es数据批量导入)

在我们项目上线之前,我们需要把数据库中的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制就能避免一次性加载过多,防止内存溢出

在我们项目中,曾有一次需要把MySQL中的一千万条历史文章同步到ES。为了避免一次性加载引发OOM,我将数据分页为每页2000条,使用线程池批量提交导入任务,同时使用 CountDownLatch 控制主线程阻塞等待所有子任务完成,再统一执行收尾逻辑。这样做极大优化了内存占用和同步效率。

DB(一千万) → 线程池(CountDownLatch) → Elasticearch

       批量导入  →  查询总条数   →       DB
                      ↓               ↑          批量导入到ES中     →   ES
(固定每页2000条)        计算总页数            ↑  (countDownLatch.countDown())
                        ↓               ↑                ↑
(总页数)         CountDownLatch        ↑                ↑
                        ↓               ↑                ↑
                分页查询文章数据 → [查询当前页的文章 → 创建任务批量导入ES → 提交到线程池执行]循环
                                             (文章列表, countDownLatch)
                                                          ↓
                                                countDownLatch.await()

线程池的使用场景②:数据汇总

  • 在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息;这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢?
    • 在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
      [统计的图文发布量、点赞数量、收藏数量、评论数量若不在同一台微服务下 或者 部分没有依赖关系]

并发数据汇总(如订单数据聚合)

关键词:接口无依赖、加速响应、Future并发调用

📌 适用场景

  • 一个接口需要聚合多个来源服务(如订单服务、商品服务、物流服务)
  • 服务之间无强依赖,可并发发起请求提升响应速度

✅ 技术点

  • 线程池 + Future + Callable
  • 三个子任务并发发起 → .get()阻塞获取返回值
  • 总响应时间 ≈ 最慢的那个接口,而不是三个之和

线程池的使用场景③:异步调用

为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法相应时间

如何控制某个方法允许并发访问线程的数量

Semaphore信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量
适用场景
通常用于那些资源有明确访问数量限制的场景,常用于限流

Semaphore使用步骤
  • 创建Semaphore对象,可以给一个容器
  • semaphore.acquire():请求一个信号量,这时候的信号量个数 -1 (一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
  • semaphore.release():释放一个信号量,此时信号量个数 +1

使用 Semaphore 信号量

  • Semaphore 是 JUC 包下的并发工具类,用于控制同时访问某个资源的线程数量
  • 底层基于 AQS(AbstractQueuedSynchronizer)实现
  • 常用于限流、并发资源控制、连接池管理、接口控制等场景
Semaphore 就像操作系统中的“通行证/信号灯”,只有拿到令牌(acquire)才能进入方法执行,执行完后必须释放令牌(release),否则其他线程会一直阻塞在那等令牌释放。
  • new Semaphore(n):设置可同时访问的线程数量为 n
  • semaphore.acquire():申请令牌,获取不到则阻塞
  • semaphore.release():释放令牌,通知其他线程

Semaphore 可以限制方法的并发访问线程数,常用于限流或控制资源并发度。通过 acquire() 获取访问许可、release() 释放许可,从而确保同时最多只有固定数量的线程能访问目标方法或资源。

谈一谈你对ThreadLocal的理解

ThreadLocal 是 Java 提供的线程本地变量工具类,用于实现线程间的数据隔离,也可理解为线程级别的“共享变量”。

🌟 一句话总结(记住这个)

ThreadLocal 实现线程间变量隔离,让每个线程拥有一份自己的变量副本,常用于解决共享变量的线程安全问题。

📌 附加:ThreadLocal ≠ 线程安全

ThreadLocal 并不是让对象变“线程安全”,只是让每个线程用自己那份数据,避免共享导致的问题。

🚀 作用与优势

  • 线程隔离:每个线程持有一份独立变量,互不干扰,解决并发线程对共享资源读写冲突的问题。
  • 线程内共享:同一线程中可以在不同方法、组件间共享数据(如事务控制、用户上下文等)。

🧠 原理说明(核心)

每个线程内部都维护一个 ThreadLocalMap(它是 Thread 类的成员变量):

  • 当调用 threadLocal.set(value) 时:
    • 当前线程的 ThreadLocalMap 中以 threadLocal 实例作为 key,value 作为值进行存储。
  • 当调用 threadLocal.get() 时:
    • 会从当前线程中以 threadLocal 为 key 查找对应的值。
  • remove() 用于手动移除,防止内存泄漏。

✅ 本质上:ThreadLocal 并不是把值保存在自己内部,而是保存在当前线程的 ThreadLocalMap 中。

☢️ 内存泄漏问题

  • ThreadLocalMap 中的 key 是 ThreadLocal弱引用,但 value 是强引用
  • 当 ThreadLocal 实例被 GC 回收后,key 变成 null,**value 仍存在,若不手动 remove,就会造成内存泄漏**
  • 最佳实践:用完一定要调用 remove() 方法清除数据
  • ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】避免争用引发的线程安全问题
  • ThreadLocal 同时实现了线程内的资源共享
  • 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
    • 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线
      程的 ThreadLocalMap 集合中
    • 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中査找关联的资源值
    • 调用remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
  • ThreadLocal内存泄漏问题ThreadLocalMap 中的key是弱引用,值为强引用; key会被Gc释放内存,关联 value的内存并不会释放。建议主动remove 释放 key,value
ThreadLocal概述

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享

案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。

ThreadLocal基本使用

  • set(value) 设置值
  • get() 获取值
  • remove() 清除值
ThreadLocal的实现原理 & 源码解析

ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离

JVM相关面试题

什么是JVM?

JVM = Java Virtual Machine 是java程序的运行环境
JVM是运行在操作系统中的 屏蔽了操作系统的差异

好处

  • 一次编码,到处运行
  • 自动内存管理,垃圾回收机制

什么是程序计数器?

程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址

javap -v xx.class:打印堆栈大小,局部变量的数量和方法的参数

找到Application的class文件后 → Build → Rebuild Project编译一下 → 找到该Application的class文件黄色的 → Open in → Terminal → javap -v Application.class

你能给我详细介绍Java堆吗?

线程共享的区域:主要用来保存对象实例、数组等,当堆中没有内存空间可分配给实例,也无法再扩展,则抛出OutOfMemoryError异常

  • 组成:年轻代 + 老年代
    • 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区幸存者区
    • 老年代主要保存生命周期长的对象,一般是一些老的对象
  • jdk1.7和1.8的区别
    • 1.7中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
    • 1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出

什么是虚拟机栈?

Java Virtual machine Stacks(Java虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

垃圾回收是否涉及栈内存?

不涉及,因为垃圾回收主要指的是堆内存
这里当栈帧弹栈后,内存就会释放

栈内存分配越大越好吗?

未必,默认的栈内存通常为1024k
栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半

方法内的局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

什么情况下会导致栈内存溢出?

栈帧过多导致栈内存溢出,经典问题:递归调用
栈帧过大导致栈内存溢出

堆栈的区别是什么?

栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会
栈内存是线程私有的,而堆内存是线程共有的。
两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.ang.OutOfMemoryError。

能不能解释一下方法区

  • 方法区(Method Area)是各个线程 共享的内存区域
  • 主要存储类的信息、运行时常量池
  • 虚拟机启动的时候创建,关闭虚拟机时释放
  • 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace
常量池

可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
Terminal中执行:javap -v Application.class
可以查看字节码结构 (类的基本信息、常量池、方法定义)
当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

你听过直接内存吗?

直接内存:并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见于NIO操作时,用于数据缓冲区,它分配回收成本较高,但读写能力高。[平时的是BIO]

直接内存并不属于JVM中的内存结构,不由VM进行管理。是虚拟机的系统内存常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理

什么是类加载器,类加载器有哪些?

类加载器

JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来

  1. 引导类加载器(Bootstrap ClassLoader)加载JAVA_HOME/jre/lib目录下的库
    • 这是最顶层的类加载器,它用于加载Java的核心库,这些库位于<JAVA_HOME>/jre/lib目录(比如rt.jarresources.jar等),或者被-Xbootclasspath参数指定的路径中。
    • 引导类加载器是用原生代码(如C/C++)实现的,它属于JVM的一部分。
    • 它并不继承自java.lang.ClassLoader,而是由JVM自身实现。
  2. 扩展类加载器(Extension ClassLoader)加载JAVA_HOME/jre/lib/ext目录中的类
    • 它负责加载<JAVA_HOME>/lib/ext目录中,或者由系统属性java.ext.dirs指定的路径中的类库。
    • 它是sun.misc.Launcher$ExtClassLoader类的实例。
  3. 系统类加载器(System ClassLoader)用于加载classPath下的类
    • 也称为应用类加载器(Application ClassLoader),它负责加载用户类路径(Classpath)上的所有类库。
    • 系统类加载器是sun.misc.Launcher$AppClassLoader类的实例。
    • 它是程序中默认的类加载器,可以通过ClassLoader.getSystemClassLoader()方法获取。
  4. 自定义加载器(CustomizeClassLoader)自定义继承ClassLoader,实现自定义类加载规则
    • 用户还可以自定义类加载器。自定义类加载器通过继承java.lang.ClassLoader类并重写相应的方法来实现。自定义类加载器可以用于特定的需求,例如在Web容器中加载类,或者在运行时从网络或其他地方动态加载类。

什么是双亲委派模型?

加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类

JVM为什么采用双亲委派机制?
  • 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
  • 为了安全,保证类库API不会被修改

说一下类装载的执行过程?

加载:查找和导入class文件
验证:保证加载类的准确性
准备:为类变量分配内存并设置类变量初始值
解析:把类中的符号引用转换为直接引用
初始化:对类的静态变量,静态代码块执行初始化操作
使用:JVM 开始从入口方法开始执行用户的程序代码
卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象

类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)

  • 通过类的全名,获得类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
  • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

对象什么时候可以被垃圾器回收

如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收

怎么确定什么是垃圾?
  • 引用计数法

    一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收

  • 可达性分析算法

    采用的都是通过可达性分析算法来确定哪些内容是垃圾

JVM垃圾回收算法有哪些?

  • 标记清除算法

    是将垃圾回收分为2个阶段,分别为标记清除

    • 根据可达性分析算法得出的垃圾进行标记
    • 对这些标记为可回收的内容进行垃圾回收
  • 复制算法

    将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低

  • 标记清理算法一般用于老年代

    标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低

JVM的分代回收是什么?

分代收集算法

在java8时,堆被分为了两份:新生代和老年代[1:2]
对于新生代,内部又分为了三个区域,Eden区,幸存者区survivor(分成from和to)【8:1:1】

MinorGC、MixedGC、FullGC的区别是什么
  • MinorGC(youngGC)发生在新生代的垃圾回收,暂停时间短(STW)
  • MixedGC:新生代 + 老年代 部分区域的垃圾回收,G1收集器特有
  • FullGC:新生代 + 老年代 完整垃圾回收,暂停时间长(STW),应尽力避免

STW(Stop-The-World)暂停所有应用程序线程,等待垃圾回收的完成

JVM有哪些垃圾回收器?

在jvm中,实现了多种垃圾收集器,包括:

  • 串行垃圾收集器

    SerialSerial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑

    • Serial 作用于新生代,采用复制算法
    • Serial Old 作用于老年代,采用标记-整理算法垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
  • 并行垃圾收集器

    Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器

    • Parallel New作用于新生代,采用复制算法

    • Parallel Old作用于老年代,采用标记-整理算法

      垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

  • CMS(并发)垃圾收集器

    CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行

  • G1垃圾收集器

    作用在新生代和老年代

详细聊一下G1垃圾回收器

  • 应用于新生代和老年代,在JDK9之后默认使用G1
  • 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专为大对象准备
  • 采用复制算法
  • 响应时间与吞吐量兼顾
  • 分成三个阶段:新生代回收(STW)、并发标记(重新标记STW)、混合收集
  • 如果并发失败(即回收速度赶不上创建新对象速度),就会触发Full GC

强引用、软引用、弱引用、虚引用的区别

强引用:只要所有 GC Roots 能找到,就不会被回收
软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够时候会回收软引用对象
弱引用:需要配合WeakReference使用,只要进行了垃圾回收,就会把引用对象回收
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存

  • 强引用:只有所有 GCRoots 对象都不通过【强引用】 引用该对象,该对象才能被垃圾回收
User user = new User();

GC Root → User对象

  • 软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收
User user = new User();
SoftReference softReference = new SoftReference(user);

GC Root → SoftReference对象 →→虚线 User对象
一开始并不会对User对象进行回收 此时User对象就是软引用 如果内存还是不够 马上又再次进行了垃 圾回收 此时软引用的User就会被回收

  • 弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
User user = new User();
WeakReference weakReference = new WeakReference(user)

GC Root → WeakReference对象 →→虚线 User对象

延申话题:ThreadLocal内存泄露问题

static class Entry extends WeakReference<ThreadLocal<?>>{
    Object value;
 Entry(ThreadLocal<?>k, Object v){
     super(k); // k是弱引用
     value = v; // 强引用,不会被回收
 }
}
  • 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
User user = new User();
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference(user, queue);

JVM调优的参数可以在哪里设置?

  • war包部署在tomcat中设置

    修改 TOMCAT_HOME/bin/catalina.sh 文件
    D:\apache-tomcat-8.5.93\bin\catalina.sh【卡特琳娜】

    # OS specific support.  $var _must_ be set to either true or false.
    JAVA_OPTS="-Xms512m -Xmx1024m"
    cygwin=false
    darwin=false
    os400=false
    hpux=false
    
  • jar包部署在启动参数设置

    通常在linux系统下直接加参数启动SpringBoot项目—VM

    nohup java -Xms512m -Xmx1024n -jar xxxx.jar --spring.profiles.active=prod &

nohup:用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
**参数&**:让命令在后台执行,终端退出后命令仍然执行

JVM调优的参数都有哪些?

对于JVM调优,主要就是调整 年轻代、老年代、元空间 的内存大小及使用的垃圾回收器类型

  • 设置堆空间大小

    设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值

    -Xms: 设置堆的初始化大小
    -Xmx: 设置堆的最大大小
    // 不指定单位默认为字节
    -Xms:1024
    -Xms:1024k
    

    堆内存设置多少合适?

    • 最大大小的默认值是物理内存的1/4,初始大小是物理内存的1/64【不设置的情况下】
    • 堆太小,可能会频繁的导致年轻代和老年代的垃圾回收,会产生STW,暂停用户线程
    • 堆内存大肯定是好的,存在风险,假如发生了fullgc,它会扫描整个堆空间,暂停用户线程的时间长
  • 虚拟机栈的设置

    虚拟机栈的设置:每个线程默认会开启1M的内存,用于存放栈帧、调用参数、局部变量等,但一般256K就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

    -Xss 对每个线程stack大小的调整,-Xss128k

  • 年轻代中Eden区和两个Survivor区的大小比例

    设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。通过增大Eden区的大小来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。

    -XXSurvivorRatio=8,表示年轻代中的分配比率:survivor:eden = 2:8

  • 年前代晋升老年代阈值【默认值为15,取值范围0-15】

    -XX:MaxTenuringThreshold=threshold

  • 设置垃圾回收收集器

    通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器

    -XX:+UseParallelGC
    -XX:+UseParallelOldGC

    -XX:+UserG1GC

JVM调优的参数都有哪些?

  • 命令工具

    • jps 进程状态信息

    • jstack 查看进程内线程的堆栈信息产生死锁可以查看

    • jmap 查看堆栈信息[生成堆转内存快照,内存使用信息]

      jmap -head pid 显示Java堆的信息
      jmap -dump:format=b,file=heap.hprof pid
      
      • format=b 表示以hprof二进制格式存储Java堆的内存

      • file=< filename > 用于指定快照dump文件的文件名

        dump:它是我们都可以通过工个进程或系统在某一给定的时间的快照。比如在进程崩溃时,甚至是任何时候,具将系统或某进程的内存备份出来供调试分析用,dump文件中包含了程序运行的模块信息、线程信息、堆调用信息、异常信息等数据,方便系统技术人品进行错误排查

    • jhat 堆转储快照分析工具

    • jstat JVM统计监测工具[可以用来显示垃圾回收信息、类加载信息、新生代统计信息等]

      • 总结垃圾回收统计:jstat -gcutil pid
      • 垃圾回收统计:jstat -gc pid
  • 可视化工具

    • jconsole 用于对jvm的内存,线程,类的监控, 是一个可视化工具
      D:\java\jdk-11.0.20\bin\jconsole.exe
    • VisualVM 能够监控线程,内存情况只有jdk1.8有
      D:\java\jdk1.8.0_181\bin\jvisualvm.exe

Java内存泄露的排查思路?

内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况
1、通过jmap或设置jvm参数获取堆内存快照dump
2、通过工具,VisualVM去分析dump文件,VisualVM可以加载离线的dump文件
3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
4、找到对应的代码,通过阅读上下文的情况,进行修复即可

JVM Stacks 虚拟机栈StackOverFlowError
Heap OutOfMemoryError:java heap space
Method Are/ MateSpace 方法区/元空间OutOfMemoryError: Metaspace

模拟堆空间溢出场景:-VM设置参数 → -Xmx10m

List<String> list = new ArrayList<>();
while(true){
    list.add("北京");
}
-------------------------------------------
// OutOfMemoryError:java heap space
如何排查启动闪退、运行一段时间宕机
  • 获取堆内存快照dump

    • 使用jmap命令获取运行中程序的dump文件【只有在项目运行时候才可以用】
    jmap -head pid 显示Java堆的信息
    jmap -dump:format=b,file=heap.hprof pid 【只有在项目运行时候才可以用】
    
    • 使用vm参数获取dump文件

      有的情况是内存溢出之后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式生成dump文件

    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/home/app/dumps/
    
  • VisualVM区分析dump文件

  • 通过查看堆内存的信息,定位内存溢出问题

CPU飙高排查方案与思路?

1.使用top命令查看占用cpu的情况
2.通过top命令查看后,可以查看是哪一个进程占用cpu较高
3.使用ps命令查看进程中的线程信息
4.使用jstack命令查看进程中哪些线程出现了问题,最终定位问题

  • 使用top命令查看占用cpu的情况哪个进程占用的cpu最高

    finalShell中输入 top

  • 查看进程中的线程信息 ps H -eo pid,tid,%cpu | gerp pid

  • jstack 查看进程内线程的堆栈信息产生死锁可以查看

    因为是十六进程所以要十进程转换十六进程
    直接linux输入 printf "%x\n" Pid
    然后就可以根据十六进制的去找哪个线程cpu占用
    之后查看文件是cat xxx

设计模式

框架中的设计模式 + 项目中的设计模式

简单工厂模式

简单工厂包含如下角色

  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品 :实现或者继承抽象产品的子类
  • 具体工厂:提供了创建产品的方法,调用者通过该方法来获取产品。

需求:设计一个咖啡店点餐系统。
设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCofee】和拿铁咖啡【LatteCoffee】); 再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。

工厂方法模式完全遵循开闭原则

方法模式的主要角色:
抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一 一对应。

抽象工厂模式

工厂方法模式只考虑生产同等级的产品,抽象工厂可以处理等级产品的生产
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产–个等级的产品,而抽象工厂模式可生产多个等级的产品。一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂

策略模式

  • 该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户
  • 它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理

策略模式的主要角色如下:
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
环境(Context)类:持有一个策略类的引用,最终给客户端调用。

策略模式—登录案例 (工厂模式 + 策略模式)

  • 什么是策略模式

    • 策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户

    • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中

  • 案例(工厂方法+策略)

    • 介绍业务(登录、支付、解析excel、优惠等级…)
    • 提供了很多种策略,都让spring容器管理
    • 提供一个工厂:准备策略对象,根据参数提供对象

一句话总结:只要代码中有冗长的if-else 或switch 分支判断都可以采用策略模式优化

举一反三

  • 订单的支付策略(支付宝、微信、银行卡..)
  • 解析不同类型excel(xls格式、xlsx格式)
  • 打折促销(满300元9折、满500元8折、满1000元7折..)
  • 物流运费阶梯计算(5kg以下、5-10kg、10-20kg、20kg以上)

策略模式和工厂方法模拟.png

责任链模式—概述及案例

责任链模式:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者(Concreate Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类(Cient)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

举一反三

  • 内容审核(视频、文章、课程)
  • 订单创建
  • 简易流程审批

区分责任链模式策略模式 在if的情况下

✅ 一句话区分

模式 一句话理解
策略模式 多种方案中选一个执行(if 就是“选择谁”)
责任链模式 多个处理器依次尝试处理(if 是“要不要接着传下去”)

👀 从 if 角度看

模式 if 的含义 结果
策略模式 选择哪个处理逻辑(只执行一个 一旦匹配执行完毕,不再走其他
责任链模式 是否要处理或继续传递(可能多个都执行 可以处理多个,也可以提前终止

🎯 场景类比(超级形象)

📦 策略模式:

像点菜:你从多个菜中选择一个最合适的吃
🔁 选择一个策略就完事

if (type == "微信") {
   payWithWeChat();
} else if (type == "支付宝") {
   payWithAliPay();
}
  • 策略模式核心:同一个接口,不同实现类,运行时选择哪个

🛠️ 责任链模式:

像审批流程:你写了个请假单,先经理签字 → 主管签字 → HR签字
🔁 每个人看一下自己要不要处理,处理完能不能往下传

if (金额 <= 1000) {
   主管处理
   return
}
if (金额 <= 5000) {
   经理处理
   return
}
HR处理
  • 责任链核心:链式传递,一个接一个处理(可中断)

✅ 示例代码对比

🧪 策略模式(支付选择)

public interface PayStrategy {
    void pay();
}

public class WeChatPay implements PayStrategy {
    public void pay() { System.out.println("微信支付"); }
}

public class AliPay implements PayStrategy {
    public void pay() { System.out.println("支付宝支付"); }
}

// 使用
PayStrategy strategy;
if ("wechat".equals(type)) {
    strategy = new WeChatPay();
} else {
    strategy = new AliPay();
}
strategy.pay();

✔️ 选择一个策略类并执行,if 只是为了选哪一个


🧪 责任链模式(审批流程)

public abstract class Approver {
    protected Approver next;

    public void setNext(Approver next) {
        this.next = next;
    }

    public abstract void process(int amount);
}

public class Manager extends Approver {
    public void process(int amount) {
        if (amount <= 1000) {
            System.out.println("Manager approved");
        } else if (next != null) {
            next.process(amount);
        }
    }
}

public class Director extends Approver {
    public void process(int amount) {
        if (amount <= 5000) {
            System.out.println("Director approved");
        } else if (next != null) {
            next.process(amount);
        }
    }
}

// 链式构建
Manager m = new Manager();
Director d = new Director();
m.setNext(d);

// 发起请求
m.process(3000);

✔️ 多个处理器按顺序尝试,if 是为了决定自己是否处理,并可能往下传递

if 语句长得一样,但目的不同:

  • 策略 if 是“选哪一个”
  • 责任链 if 是“要不要传下去”

常见技术场景题

单点登录这块怎么实现的?

单点登录的英文名:Single Sign On (SSO),只需要登录一次,就可以访问所有信任的应用系统

① 先解释什么是单点登录:单点登录的英文名叫做:Single SignOn(简称SSO)
② 介绍自己项目中涉及到的单点登录(即使没涉及过,也可以说实现的思路)
③ 介绍单点登录的解决方案,以JWT为例
用户访问其他系统,会在网关判断token是否有效
如果token无效则会返回401(认证失败)前端跳转到登录页面
用户发送登录请求,返回浏览器一个token,浏览器把token保存到cookie
再去访问其他服务的时候,都需要携带token,由网关统一验证后路由到目标服务

权限认证是如何实现的?

后台的管理系统,更注重权限控制,最常见的就是RBAC模型来指导实现权限
RBAC(Role-Based Access Control)基于角色的访问控制

  • 3个基础部分组成:用户、角色、权限
  • 具体实现:
    • 5张表:用户表、角色表、权限表、用户角色中间表、角色权限中间表
    • 7张表:用户表、角色表、权限表、菜单表、用户角色中间表、角色权限中间表、权限菜单中间表

张三具有什么权限呢?
流程:张三登录系统 → 查询张三拥有的角色列表 → 再根据角色查询拥有的权限

权限框架:Apache shiroSpring Security(推荐)

上传数据的安全性你们怎么控制?

主要说的是数据在网络上传输如何保证安全

使用**非对称加密(或对称加密)**,给前端一个公钥让他把数据加密后传到后台,后台负责解密后处理数据

对称加密

文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥

你负责项目的时候遇到了哪些比较棘手的问题?怎么解决的?1+3

其次你也可以说说aop的实现,比如你们操作日志记录等,利用aop切面思想,通过环绕通知等但需封装出出个切面工具类。建议你们说说sql调优,比如商品列表页需要分页查询,但是几百万商品导致查询慢,如何优化的,这是一个

① 设计模式在项目中的应用

是为了遵循一系列的开发原则【工厂、策略、责任链】

  • 什么背景[技术问题] → 登录的例子
  • 过程[解决问题的过程]
  • 最终落地方案
② 线上BUGJVM+多线程
  • CPU飙高
  • 内存泄露
  • 线程死锁
③ 调优
  • 慢接口
  • 慢SQL
  • 缓存方案

④ 组件封装

  • 分布式锁
  • 接口幂等
  • 分布式事务
  • 支付通用
你们项目中日志怎么采集的?

我们搭建了ELK日志采集系统
介绍**ELK**的三个组件:
Elasticsearch是全文搜索分析引擎,可以对数据存储、搜索、分析
Logstash是一个数据收集引擎,可以动态收集数据,可以对数据进行过滤、分析,将数据存储到指定的位置
Kibana是一个数据分析和可视化平台,配合Elasticsearch对数据进行搜索,分析,图表化展示

  • 为什么要采集日志

日志是定位系统问题的重要手段,可以根据日志信息快速定位系统中的问题

  • 采集日志的方式有哪些
    • ELK:即ElasticSearch、LogStash、Kibanna三个软件的首字母
    • 常规采集:按天保存到一个日志文件

查看日志的命令?查看是否在线查看过日志
  • 实时监控日志的变化
    实时监控某一个日志文件的变化:tail -f xx.log
    实时监控日志文件最后100行的变化:tail -n 100 -f xx.log

  • 按照行号查询
    查询日志尾部最后100行日志:tail -n 100 xx.log
    查询日志头部开始100行日志:head -n 100 xx.log
    查询某一个日志行号区间:cat -n xx.log | tail -n +100 | head -n 100(查询100行至200行的日志)

  • 按照关键字找日志的信息
    查询日志文件中包含debug的日志行号:cat -n xx.log | grep "debug"

  • 按照日期查询日期必须在日志中出现过

    sed -n '/2025-01-14 14:22:31.070/,/ 2025-01-14 14:27:18.158/p' xx.log

  • 日志太多,处理方式

    • 分页查询日志信息:cat -n xx.log | grep "debug" | more
    • 筛选过滤后,输出到一个文件:cat -n xx.log | grep "debug" > debug.txt

上线的项目远程Debug —— 生产问题怎么排查?本地调试远程代码

已经上线的bug排查的思路:

  • 先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题
  • 远程debug(通常公司的正式环境(生产环境)是不允许远程debug的。一般远程debug都是公司的测试环
    境,方便调试代码)

远程debug

前提条件:远程的代码和本地的代码要保持一致

远程代码需要配置启动参数,把项目打包放到服务器后启动项目的参数:

java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 project-1.0-SNAPSHOT.jar

idea中设置远程debug,找到idea中的Edit Configurations... → 添加一个Remote JVM debug → 右侧要配置Configuration的Host → 添加上面的代码…

在项目中点debug(绿色小虫子)

访问远程服务器,在本地代码中打断点即可调试远程

怎么快速定位系统的瓶颈?

  • 压测(性能测试),项目上线之前测评系统的压力

    • 压测目的:给出系统当前的性能状况;定位系统性能瓶颈或潜在性能瓶颈
    • 指标:响应时间、QPS、并发数、吞吐量、CPU利用率、内存使用率、磁盘IO、错误率
    • 压测工具:LoadRunner、Apache Jmeter …
    • 后端工程师:根据压测的结果进行解决或调优(接口、代码报错、并发达不到要求.)
  • 监控工具、链路追踪工具,项目上线之后监控

    • 监控工具:Prometheus+Grafana
    • 链路追踪工具:skywalking、Zipkin
  • 线上诊断工具Arthas(阿尔萨斯),项目上线之后监控、排查

    • 官网:https://arthas.aliyun.com/

    • 核心功能:Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。
      当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:

      • 这个类从哪个jar 包加载的?为什么会报各种类相关的 Exception?

      • 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?

      • 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?

      • 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!

      • 是否有一个全局视角来查看系统的运行状况?

      • 有什么办法可以监控到 JVM 的实时运行状态?

      • 怎么快速定位应用的热点,生成火焰图?

      • 怎样直接从 JVM 内查找某个类的实例?

怎么解决cpu飙高?

使用top命令查看占用cpu的情况
通过top命令查看后,可以查看是哪一个进程占用cpu较高
使用ps命令查看进程中的线程信息 使用top -H -p 进程Id [找线程哪个使用多]
记住要打印%X十六进制
使用jstack命令查看进程中哪些线程出现了问题,最终定位问题
jstack 进程PID | grep 16进制线程PID -A 20

  • 使用top命令查看占用cpu的情况哪个进程占用的cpu最高

    finalShell中输入 top

  • 查看进程中的线程信息 ps H -eo pid,tid,%cpu | gerp pid

  • jstack 查看进程内线程的堆栈信息产生死锁可以查看

    因为是十六进程所以要十进程转换十六进程
    直接linux输入 printf "%x\n" Pid
    然后就可以根据十六进制的去找哪个线程cpu占用
    之后查看文件是cat xxx

2025/1/14 20:35 地点广州 完结撒花

项目难点?四方保险——day11-数据中心:时序数据库、看板展示【实战】

技术上的难点:时序数据库看板展示

秒杀系统如何优化?

痛点描述:
  • 瞬时并发量大
    • 大量用户会在同一时间进行抢购
    • 网站瞬时访问流量激增
  • 库存少
    • 访问请求数量远远大于库存数量
    • 只有少部分用户能够秒杀成功

Ⅰ. 访问层 — 商品页

  • 可以将静态秒杀页面放在cdn上[用户访问速度↑ 减轻服务器压力++]

Ⅱ. 访问层 — 秒杀按钮

  • 活动前禁用按钮、点击后禁用按钮、滑动验证码[防羊毛党]、排队体验[提升用户体验]

Ⅲ. 中间转换层 — 多级负载均衡 & 限流 & 自动伸缩

  • 通常会通过Nginx来进行负载均衡【单台Ng处理的并发量是两三万左右】
  • 在它上层要做到硬件级别的隔离器 【F5/LVS】
  • 通过Ng负载均衡到网关之后 通过客户端的负载均衡器Ribbon
  • 4级的负载均衡 可以处理每秒上10W以上的QPS并发量
  • 通过DockerK8S来进行云服务器的动态伸缩的部署[秒杀开始自动扩容 秒杀结束自动缩减]
  • 注意要在Ng上做好限流 防止一些绕过了我们前端的DDOS攻击 还需要在网关层通过Sentinel对不同的服务节点去设置限流以及熔断的机制
  • 可以在秒杀中通过MQ做削锋填股 通过MQ可以减轻下游的压力 防止激增流量打垮下游数据库

Ⅳ. 服务端 — 用Redis做缓存减轻数据库压力

  • 秒杀商品信息预热到Redis中 防止Redis被击穿我们的数据库
  • 通过Redis的Lua脚本[保证多个操作的原子性]操作库存
  • 防重 可以通过 redis的SETNX → 用 Token + 商品URL // IP + 商品URL 只能有一个有效
  • 分布式锁保证请求的原子性 → Redisson的分布式锁

**Ⅴ. 数据库 — 读写分离 **

  • 数据量很大就分库分表

✅ 一张图理清:秒杀系统全链路优化流程

 [用户点击秒杀按钮]
        ↓
   【前端防刷】
      - 限制频繁点击
      - 滑动验证码
      - 倒计时、按钮控制
        ↓
   【网关 & Nginx】
      - 黑名单拦截(IP、UA)
      - Sentinel 限流 + 降级 + 熔断
        ↓
   【秒杀服务】
      - 判断秒杀状态、时间、库存是否存在
      - 生成秒杀Token(防重)
      - Redis 原子性扣减库存(Lua脚本)
      - 发送下单消息至 MQ 异步处理
        ↓
   【MQ异步削峰】
      - 持久化队列(RocketMQ / Kafka)
      - 消费者异步落库
        ↓
   【数据库层】
      - MySQL最终扣减库存 + 创建订单(事务)
      - 数据库读写分离 / 分库分表

🧩 各模块详细优化方案

① 前端层(第一道防线)

  • 活动页静态化:部署在 CDN,秒开页面,减少服务器并发压力。

    CDN域名是指通过内容分发网络(CDN)技术加速访问的域名。CDN的全称是Content Delivery Network,即内容分发网络。它通过将源站内容分发到分布在全球各地的加速节点,使用户可以从离自己最近的节点获取内容,从而提升访问速度和体验。

    CDN域名的工作原理是将用户的访问请求通过DNS解析,指向最优的CDN节点。如果节点上已有缓存内容,则直接返回给用户;如果没有缓存,则从源站拉取内容并缓存到节点,供后续用户访问。

  • JS 控制按钮状态:倒计时期间按钮禁用;点击后立即禁用防止重复提交。

  • 防刷机制

    • 滑动验证码(极验、腾讯滑动等)
    • 限制频繁请求(客户端节流 + 后端拦截)
    • 秒杀路径动态化(通过接口获取临时随机URL)

② 网关层(第二道防线)

  • Nginx限流 + F5/LVS 硬件负载均衡
  • Sentinel限流
    • QPS限流、线程数限制
    • 降级策略(服务不稳定时快速失败)
  • 灰度发布 + 金丝雀策略防雪崩

③ 服务层(核心逻辑)

  • Redis预热商品库存
    • key: seckill:stock:123 => 10
  • Lua脚本保证扣减原子性
if redis.call("get", KEYS[1]) >= tonumber(ARGV[1]) then
   return redis.call("decrby", KEYS[1], ARGV[1])
else
   return -1
end
  • Token校验防重(防止同一用户多次提交)
    • 用户下发秒杀Token
    • 下单时校验 token 是否存在
  • 幂等性处理:幂等令牌、Redis标记等手段防止重复下单
  • Redisson分布式锁(用于控制某些全局状态,如每秒限量)

④ MQ 消息队列层(削峰填谷)

  • ✅ 典型架构:RocketMQ / RabbitMQ / Kafka
  • ✅ 一进一出,异步下单逻辑
    • 消息格式:包含userId, productId, token
  • ✅ 消息失败怎么办?
    • 死信队列 + 重试机制 + 日志报警

⑤ 数据库层(最终一致性)

  • 分库分表
    • 订单表按用户ID或时间范围分表
    • 库存表按商品类型分表
  • 读写分离
    • MySQL主从复制
    • 下单写入主库,查询走从库
  • 事务处理
    • 扣库存 + 创建订单需要事务包裹
  • 补偿机制
    • MQ失败回滚机制 + 自动重试 or 人工介入

🎯 秒杀系统关键点总结(重点记忆)

优化维度 关键点
防刷防作弊 滑动验证码、动态路径、限流、IP黑名单
限流削峰 Sentinel、MQ异步下单、排队
高性能扣减 Redis + Lua 脚本,原子扣减库存
数据一致性 MQ消息可靠投递、事务补偿机制
分布式扩展 分库分表、读写分离、动态扩容
安全性 Token校验、防重、防止超卖

我将为你详细讲解和设计一个真实的秒杀系统完整优化方案,从 Redis 预热、限流、库存扣减、异步下单、订单状态回写等关键步骤一一展开说明,并配上示意代码。


🔧 一、整体秒杀流程概览图

用户请求 → 接入层限流 → Redis预扣库存(Lua脚本) → 发送MQ消息 → 异步下单 → 数据库落库 → 回写订单状态

🧱 二、Redis缓存预热(秒杀前的准备工作)

提前将商品库存加载到 Redis,避免高并发时频繁访问数据库。

// Redis结构设计
// key: seckill:stock:<skuId>
// val: 商品库存数量

public void preloadSeckillStock(Long skuId, Integer stock) {
    String key = "seckill:stock:" + skuId;
    redisTemplate.opsForValue().set(key, stock);
}

🛡 三、限流 + 防刷 + 验签(接入层)

使用网关 Sentinel 限流,前端限制点击频率,后端防刷接口做风控。

@GetMapping("/doSeckill")
public ResponseEntity<?> doSeckill(@RequestParam Long skuId) {
    // 判断是否登录
    Long userId = getLoginUserId();
    
    // 判断是否重复请求(防重)
    String repeatKey = "seckill:user:" + userId + ":sku:" + skuId;
    Boolean hasBought = redisTemplate.opsForValue().setIfAbsent(repeatKey, "1", 5, TimeUnit.MINUTES);
    if (!hasBought) return ResponseEntity.status(429).body("请勿重复抢购");

    // 执行扣库存的 Lua 脚本
    Long result = redisTemplate.execute(luaScript, Collections.singletonList("seckill:stock:" + skuId), "1");
    if (result == 0L) return ResponseEntity.status(410).body("库存不足");

    // 发送消息至 MQ 进行异步处理
    SeckillMessage msg = new SeckillMessage(userId, skuId);
    mqTemplate.convertAndSend("seckill.queue", msg);

    return ResponseEntity.ok("下单请求已提交");
}

📜 四、Lua脚本操作 Redis(保证原子性)

-- KEYS[1]: 库存key
-- ARGV[1]: 扣减数量
local stock = redis.call("get", KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
    return redis.call("decrby", KEYS[1], ARGV[1])
end
return 0

📦 五、MQ异步消费 + 创建订单(核心业务)

用 RabbitMQ、RocketMQ、Kafka 等异步落库下单,减轻主线程压力。

@RabbitListener(queues = "seckill.queue")
public void handleSeckill(SeckillMessage msg) {
    Long userId = msg.getUserId();
    Long skuId = msg.getSkuId();

    // 检查数据库是否已下单(防止重复下单)
    boolean exists = orderMapper.existsByUserAndSku(userId, skuId);
    if (exists) return;

    // 创建订单
    Order order = new Order();
    order.setUserId(userId);
    order.setSkuId(skuId);
    order.setStatus("WAIT_PAY");
    orderMapper.insert(order);
}

🧨 六、下单后定时关闭未支付订单(延迟消息)

利用 RabbitMQ 的延迟队列(或用 Redis 的 ZSet+定时任务轮询)。

// 下单后发送延迟消息
rabbitTemplate.convertAndSend("order.ttl.exchange", "order.ttl", orderId);

// 死信队列处理超时未支付订单
@RabbitListener(queues = "order.dlx.queue")
public void closeOrder(String orderId) {
    Order order = orderMapper.selectById(orderId);
    if ("WAIT_PAY".equals(order.getStatus())) {
        order.setStatus("CLOSED");
        orderMapper.updateById(order);

        // 回滚库存
        redisTemplate.opsForValue().increment("seckill:stock:" + order.getSkuId());
    }
}

📊 七、数据一致性考虑

场景 解决方案
Redis库存扣减成功但消息发送失败 使用本地事务 + MQ事务消息机制
订单超时未支付但库存没回滚 MQ死信队列 + 回滚库存
秒杀重复下单 Redis防重 + DB唯一约束

✅ 八、技术选型建议

模块 技术
缓存 Redis
消息队列 RabbitMQ/RocketMQ
限流防刷 Sentinel、滑动窗口
延迟任务 MQ延迟队列、定时任务轮询
Redisson 分布式锁
脚本原子性 Lua脚本


你说的很好 我想更进一步了解一下 你说的 Nginx限流+F5/LVS硬件负载均衡;网关&nginx黑名单拦截(IP、UA);生成秒杀Token;灰度发布+金丝雀策略

✅ 一、Nginx 限流 + F5/LVS 硬件负载均衡

1. Nginx 限流

✨目的:

  • 限制单位时间的请求数,防止恶意刷接口、瞬间高并发导致服务崩溃。

✨配置方式:

http {
    limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=1r/s;
    ...
    server {
        location /seckill {
            limit_req zone=req_limit_per_ip burst=5 nodelay;
            proxy_pass http://seckill-server;
        }
    }
}

参数解释:

  • rate=1r/s:每秒允许一个请求。
  • burst=5:允许瞬间突发5个请求。
  • nodelay:立即处理突发请求,不排队。

✅应用场景:

  • 秒杀接口、登录接口、验证码接口等敏感接口的访问频控。

2. F5 / LVS 硬件负载均衡

✨作用:

  • F5/LVS 作为物理网络设备层面的高性能负载均衡系统,位于企业最外层(防火墙之后,Nginx之前),进行四层(TCP/UDP)转发,性能远高于 Nginx。

✅典型架构:

markdown复制编辑             用户请求
                 ↓
       ┌────────────────┐
       │  F5 / LVS  (L4)│
       └────────────────┘
                 ↓
       ┌────────────────┐
       │   Nginx (L7)    │
       └────────────────┘
                 ↓
            应用服务层

✅优势:

  • 性能高、吞吐量大(每秒几十万 QPS)。
  • 更稳定,更适合企业级大并发业务。
  • 通常和 Nginx 搭配使用,分别处理 L4 / L7 流量调度。

✅ 二、网关 & Nginx 黑名单拦截(IP/UA)

✅ 目标:

防止恶意攻击、秒杀脚本、爬虫工具参与秒杀。

1. 拦截策略

  • IP 黑白名单:将恶意访问源加入黑名单。
  • UA 校验:不允许 curl、httpclient、python 等工具模拟请求。
  • 访问频率统计:单位时间内访问频率超过阈值即封禁。

2. 网关层实现(如 SpringCloud Gateway)

// 伪代码:过滤器中处理
if (blackList.contains(ip) || suspiciousUA(ua)) {
    return 403 Forbidden;
}

✅ 三、生成秒杀Token(防黄牛、刷请求)

✅ 核心思想:

用户先请求一个随机 Token,再带着这个 Token 才能参与秒杀,且只能用一次

✅ 实现流程:

🔁 第一步:获取秒杀Token

GET /seckill/token?productId=123
Header: Authorization: Bearer 用户登录Token

返回:

{ "token": "a8729f03-6a22-4cdb-8ad4-e19932cf" }

Redis 存储 token → seckill:token:uid:productId -> token

🏃 第二步:用户秒杀请求带上Token

POST /seckill/submit
Body: { productId: 123, token: "a8729f03-6a22-4cdb-8ad4-e19932cf" }

✅ 服务端校验:

  • token是否存在;
  • token是否属于当前用户;
  • 校验通过后删除 token(一次性);
  • 进入库存扣减逻辑。

✅ 四、灰度发布 + 金丝雀策略(保障系统稳定性)

✅ 区别:

策略 说明
灰度发布 指在小范围(如某些用户/机器)上逐步发布新功能
金丝雀发布 类似灰度,但更强调监控与回滚能力

✅ 示例:

1. 灰度规则:

  • 仅对北京地区、特定用户ID段、Android设备开放新功能;
  • 灰度用户看到的是新接口,其他人访问旧接口。

2. 实现方式:

  • 在网关中增加灰度判断逻辑,如基于用户ID、设备类型、地理位置等维度;
  • 配合 Nacos / Apollo 实现灰度配置动态下发。
if (grayUser(userId)) {
    routeTo(newServiceV2);
} else {
    routeTo(oldServiceV1);
}

3. 金丝雀监控:

  • 接入 Prometheus + Grafana;
  • 实时监控新版本的接口成功率、响应时间、异常率;
  • 若指标异常,立即自动或手动回滚。

🧠 总结:秒杀防护层层递进

层级 技术 / 策略 目的
网络层 F5/LVS、Nginx限流 硬抗高并发
网关层 黑名单拦截、UA过滤、灰度/金丝雀 防刷 + 风控
应用层 秒杀Token、验证码、人机校验 精准控制用户行为
缓存层 Redis库存控制、Lua原子操作、布隆过滤器 高性能、数据一致
后端层 MQ异步削峰、数据库分库分表 解耦 + 弹性架构

订单超时自动取消是怎么实现的?

① JDK自带的延时队列

优点:简单,不需要借助其他第三方组件,成本低。
缺点:所有超时处理订单都要加入到DelayQueue中,占用内存大,没办法做到分布式处理,之恶能在集群中挑选一台leader专门处理,效率低
不适合订单量比较大的

② 基于RocketMQ的定时消息 — 延时消息

优点:使用简单,和使用普通消息一样,支持分布式。精度高,支持任意时刻

缺点使用限制:定时时长最大值24小时。
成本高:每个订单需要新增一个定时消息,且不会马上消费,给MQ带来很大的存储成本。
同一个时刻大量消息会导致消息延迟:定时消息的实现逻辑需要先经过定时存储等待触发,定时时间到达后才会被投递给消费者。因此,如果将大量定时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。

③ 基于Redis的过期监听

设置过期时间:24小时内没有支付就会自动取消
缺点:(也是所有中间件的缺点)

  • 不可靠 Redis在过期通知的时候,如果应用正好重启了,那么就有可能通知事件就丢了,会导致订单一直无法关闭,有稳定性问题。如果一定要使用Redis过期监听方案,建议再通过定时任务做补偿机制。
  • 如果订单量大需要占用中间件大量的存储空间,需要额外维护成本。
④ 定时任务分布式处理【要按照成本思维的思考方式】

通过定时任务(任务调度)的批量处理 → 一次性把所有超时的订单全部捞出来 处理完再全部执行更新
如果使用中间件都要单独存储那些数据,如果存储压力大就要涉及到集群

如果对于超时精度比较高,超时时间在24小时内,且不会有峰值压力的场景下,推荐使用RocketMQ的定时消息解决方案
在电商业务下,许多订单超时场景都在24小时以上,对于超时精度没那么敏感,并且有海量订单需要批处理,推荐使用基于定时任务的跑批解决方案。

✅ 最佳实践对比表

方案 分布式支持 精度 可靠性 适用场景 优缺点总结
DelayQueue ❌ 否 秒级 ❌ 低 单体项目/小订单量 简单、无需中间件,但不支持分布式和高可用
RocketMQ延时消息 ✅ 是 秒级 ✅ 高 秒杀、限时抢购、延迟关闭等 精度高、支持分布式,但时长限制 & 消息堆积
Redis过期监听 ✅ 是 秒级 ❌ 较低 轻量业务、有兜底机制的场景 实时、方便,但事件容易丢失,不适合重要任务
定时任务跑批 ✅ 是 分钟级 ✅ 高 电商订单系统、大量订单处理 稳定、灵活、适合大业务,容忍分钟级延迟

🔄 常见混合策略推荐

  • 秒杀业务 / 限时订单:RocketMQ 延时消息为主 + 补偿机制(定时任务兜底)
  • 电商平台:定时任务跑批为主 + MQ 异步通知用户(取消成功推送)
  • 轻量小应用:Redis 过期监听 + 手动补偿兜底
  • 单体项目或demo:DelayQueue 简单可用

🧠 思考:为什么不用 cron 来做?

cron 固定执行时间点,而订单创建是动态的,无法精确知道每个订单的30分钟是哪一刻。

举例:

  • cron表达式只能写 每隔5分钟扫描每天0点执行
  • 订单创建时间是不确定的 → 用cron不能实时取消30分钟后的每个订单

所以,动态调度任务(MQ/DelayQueue)或带参数处理(定时扫描数据库)更合适。

如何防止重复下单?

方案一:提交订单按钮置灰 [防止用户无意点击多次]
方案二:后端采用redis的setnx 来保证它的唯一幂等性

setnx:当我们调用setnx来去保存一个key和value的时候,如果这个value没有值的话,那么就会返回true保存成功;如果有值就会返回false → 保证多次存储只能存储一个值

业务幂等号(如唯一Token机制)

  • 用户点击下单前,后端下发一个token(存 Redis),用户下单时带上这个 token。
  • 后端验证 token 是否存在,使用后即删除。
// 伪代码
if (redisToken == null || !redisToken.equals(requestToken)) {
    throw new RuntimeException("重复请求或非法请求");
}
redisTemplate.delete(redisToken);

✅ 优点:

  • 通用幂等机制,不局限订单;
  • 可防止表单重复提交、支付回调重复通知等场景;

✅ 使用场景:

  • 秒杀下单、提交表单、支付回调。

🔐 方案四:消息队列去重(异步场景)

  • 使用 RocketMQ 的幂等机制,确保同一消息只消费一次(消费端做去重处理),适用于下单流程是异步的情况。

三、多手段组合更安全

层级 技术手段 是否强制
前端 按钮置灰/节流
应用层 Redis + setnx 或 Token
数据层 唯一约束字段
异步处理 MQ消费幂等处理

📌 典型实践示意图:

  • 用户点击下单获取唯一Token/Redis锁请求成功后释放写入订单表时校验唯一性

💡 小贴士:

  • 并发高推荐:Redis方案(+Redisson分布式锁)
  • 最后兜底:数据库唯一索引
  • 支付类、接口幂等推荐:Token机制

怎么防止刷单?【人肉机刷单!!】

业务风控

提高羊毛门槛:实名认证、消费门槛、随机优惠
限制用户参与、中奖、奖励次数
根据用户的历史行为和忠诚度,提供不同层次的优惠,优待忠实用户
奖池(优惠券数量)限制上限

分布式集群架构下怎么保证并发安全?

✅ 一、为什么分布式架构下更容易出现并发问题?

在单体应用中,所有请求在同一个进程内处理,天然可以用synchronized等方式控制并发。

而在分布式集群架构中:

  • 请求会打到多个节点 → 本地锁失效
  • 数据可能分库分表 → 数据不在一个数据库
  • 多线程 + 多机器 + 多服务 → 并发成倍放大

所以需要一整套分布式并发安全解决方案。

✅ 二、并发安全常见场景

  • 秒杀/抢购:多个用户同时抢一件商品
  • 下单:防止重复下单、超卖
  • 支付:防止重复支付
  • 分布式调度:防止定时任务重复执行
  • 分布式ID生成:避免重复ID

✅ 三、总结:解决并发的“组合拳”

组件/策略 主要作用
Redis分布式锁 跨节点并发控制,控制共享资源
乐观锁(version) 控制数据库并发更新冲突
消息队列MQ 异步削峰,提高系统吞吐
唯一Token机制 防止重复提交
限流 & 黑名单机制 拦截恶意请求,保护系统
本地+分布式缓存 缓解数据库压力,提高响应速度
灰度发布 降低风险,平稳上线

让你设计一个扫码登录怎么实现?

生成二维码

请求登录页生成二维码,PC端请求后端生成一个二维码,此时在后端就会生成一个全局唯一的二维码ID,主要保存二维码的状态[二维码ID, NEW],状态设置到Redis设置过期时间,然后把当前的二维码ID返回给前端,然后生成二维码 【前后端都可以生成 → 返回Base64的编码给前端】此时的二维码就绑定了用户的ID让用户扫描。

扫码

PC端和后端会建立一个轮询的请求,不断的根据二维码ID去查询二维码状态,一旦状态改变页面也会改变。也可以通过长连接WebSocket获取状态 淘宝用的轮询、抖音用的长连接,此时就可以扫码。
扫码前保证手机是登录状态 没有登录肯定是不能扫码的,登录后进行扫码就会携带手机端的用户token以及二维码的ID在后端去校验请求Token,如果校验成功就代表手机可以登录,此时可以变更二维码状态为扫描。前端就可以根据这个把页面变为待确认状态

✅ 一、整体流程概述

目标: 用户在 PC 端扫码并登录系统,安全、高效、用户体验好。

参与者:

  • PC 浏览器(Web)
  • 手机 App(用户已登录)
  • 后端服务(Web + API)
  • Redis(状态存储)
  • 前端轮询/长连接

🧩 二、关键技术点拆解

1️⃣ 二维码生成(PC端发起)

  • 用户打开 PC 登录页面,请求后端接口 /api/qr/generate
  • 后端逻辑:
    • 生成唯一的二维码ID(一般用 UUID、Snowflake 或 Redis Incr)
    • 创建二维码状态:[qrCodeId: {status: NEW, userId: null}] 存入 Redis,设置过期时间(如:3分钟)
    • qrCodeId 返回给前端(前端将其转成二维码图像)

二维码状态定义:

状态值 含义
NEW 二维码已生成,待扫码
SCANNED 手机已扫码,待确认
CONFIRMED 用户已确认登录
EXPIRED 二维码过期

2️⃣ 轮询 or WebSocket 监听状态(PC端)

  • 前端定时调用 /api/qr/status?qrCodeId=xxx 或使用 WebSocket 长连接订阅状态变更。
  • 后端通过 Redis 获取二维码状态,响应当前状态值给前端;
  • 前端根据状态更新 UI:
    • NEW:显示二维码提示扫码
    • SCANNED:显示“请确认登录”
    • CONFIRMED:跳转系统首页
    • EXPIRED:提示“二维码已失效”

3️⃣ 手机扫码(App 端发起)

  • 用户打开手机 App,扫码得到 qrCodeId
  • App 发起请求 /api/qr/scan,携带:
    • qrCodeId
    • 当前用户登录 Token(说明谁在扫码)
  • 后端校验 Token 合法性 + 校验二维码状态是否是 NEW
  • 如果校验通过:
    • 更新 Redis 状态为 SCANNED
    • 保存扫码用户ID(用于确认登录)

4️⃣ 手机端确认登录(App 端)

  • 用户点击“确认登录”按钮,App 发起请求 /api/qr/confirm
  • 后端再次校验 Token、状态、qrCodeId
  • 如果合法:
    • 更新 Redis 状态为 CONFIRMED
    • 后端为 PC 端生成登录凭证(JWT 或 Session ID)
    • 可以将 token 写入 Redis,让 PC 端后续使用

5️⃣ PC端轮询到 CONFIRMED 状态后

  • 前端收到 CONFIRMED 状态
  • 发起请求 /api/qr/login?qrCodeId=xxx
  • 后端从 Redis 中取出对应用户信息
  • 为 PC 创建 Session 或返回 JWT Token
  • 登录成功,跳转首页

🔐 三、安全要点

  • 二维码应设置过期时间,防止被反复使用
  • Token 校验要严谨,确保扫码者是本人
  • Redis 里状态更新使用 Lua 脚本或事务 CAS 保证一致性
  • 后端二维码状态需加密传输或限制频繁请求(防刷)

🚀 四、技术选型小结

功能 技术 说明
二维码生成 UUID + Redis 可快速唯一生成并记录状态
状态存储 Redis(带 TTL) 快速响应,高并发,易过期处理
实时通知 轮询 / WebSocket 淘宝用轮询、抖音用 WS
登录授权 JWT / Session 生成 PC 端登录凭证
防刷限流 接口限流 + 签名校验 避免恶意轮询/伪造请求

✅ 五、流程图(配合讲解)

text复制编辑[PC端]        [后端]                   [App端]
  |                      |                         |
  |--> 请求生成二维码 -->|                         |
  |                      |-- 生成qrCodeId + Redis存储
  |<-- 返回二维码Base64--|                         |
  |                      |                         |
  |==轮询/WS监听状态====>|                         |
  |                      |                         |
  |                      |<--扫码携带Token + qrCodeId
  |                      |--校验后标记为SCANNED     |
  |<==收到SCANNED状态== |                         |
  |                      |<--确认登录               |
  |                      |--更新为CONFIRMED +登录信息
  |<==收到CONFIRMED==   |                         |
  |-- 请求登录状态凭证 -->|                         |
  |<-- 返回JWT/Session--|                         |

如何设计分布式日志存储架构?

使用redis出现缓存三兄弟如何解决?减轻数据库的压力

你在项目中用到了Redis对吧 介绍一下有没有遇到关于redis的什么问题?

暂时还没看!
12.使用redis出现缓存击穿雪崩穿透怎么解决_哔哩哔哩_bilibili

如何使用Redis记录用户连续登录了多少天?放在数据库里不合适

放在数据库不合适因为你要创建一个表 记录用户哪一天进行了签到 如果用户量很多就会很大的量

这个问题其实就是一个连续签到/登录统计问题,数据库不适合是因为:

  • 每签到一天就插一行 → 数据量巨大,I/O压力大。
  • 查询连续签到天数复杂,SQL不好写,效率低。

所以使用 Redis 的 位图(bitmap) 来解决,是一种低存储+高性能的方案。


🎯 场景目标

统计用户连续登录天数本月第几天登录过,实现类似:

复制编辑用户A 7月签到状态 = 01111100010001000(1表示登录,0表示没登录)
查询当前用户是否今天登录过?
查询用户本月连续登录天数?

🧠 技术选型:使用 Redis Bitmap

什么是 Bitmap?

Redis 的 Bitmap 本质上是字符串(String 类型),但你可以对它的每一位 bit 操作:

# 设置偏移量为 5 的位置为 1(签到)
SETBIT user:sign:1001:202507 5 1

# 查询偏移量为 5 的位置是否为 1(是否登录)
GETBIT user:sign:1001:202507 5

✅ 实现思路

1. 登录/签到时

int offset = today - 1; // 7月19日 → offset = 18,从0开始
String key = "user:sign:" + userId + ":" + yyyyMM;
redisTemplate.opsForValue().setBit(key, offset, true);

2. 查询某天是否登录

Boolean isLogin = redisTemplate.opsForValue().getBit(key, offset);

3. 查询本月累计登录多少天

BitCountOptions options = BitCountOptions.defaults();
Long total = redisTemplate.execute((RedisCallback<Long>) conn -> 
    conn.bitCount(key.getBytes(), options)
);

4. 查询连续登录天数(重点)

假设今天是第19天,从 offset = 18 开始向前看:

int count = 0;
for (int i = offset; i >= 0; i--) {
    if (redisTemplate.opsForValue().getBit(key, i)) {
        count++;
    } else {
        break; // 一旦中断,退出循环
    }
}

🚀 优势

  • Redis Bitmap 单月只占用 31 bit,超省空间(一个用户一年只用 372 bit ≈ 47 字节)
  • 查询效率高,O(1)
  • 写入也快,支持并发

🔐 注意点

  • 设置过期时间:避免内存占用过大

    redisTemplate.expire(key, Duration.ofDays(60));
    
  • 如果需要“补签”功能,操作相应的 bit 位即可

📌 示例 Redis 数据结构(7月)

Key: user:sign:1001:202507

Value (bit位):0111110000000000000000000000000
日期:1 2 3 4 5 6 7 ... 31
含义:从左往右,第i位是第i+1天

给你一亿个Redis keys统计双方的共同好友?

🧩 题目解析

假设:

  • 每个用户的好友列表存储在 Redis 的 Set 结构中,例如:
    SADD friends:user1 A B C D
    SADD friends:user2 B C E F

目标:

  • 快速统计两个人共同的好友,即集合交集数量。

✅ 常规解法:Redis SINTER 命令

Redis 支持对多个集合求交集:

SINTER friends:user1 friends:user2

返回结果即为双方的共同好友。

如果只要交集数量,可以使用:

SINTERCARD 2 friends:user1 friends:user2

这是 Redis 7.0 新增的命令,效率更高

🧠 问题难点:一亿个 keys 怎么办?

一亿个 keys 说明用户量巨大或好友数量极多,可能涉及以下挑战:

问题 描述
Redis 内存压力 如果所有好友关系都存在 Redis Set 中,消耗大量内存
网络 IO 开销 获取或计算时大量命令交互
集合元素巨大 每个 Set 元素多(例如几千好友),单次 SINTER 代价大
频繁交集操作 如果这是一个高频功能(如社交推荐),需要高效方案

🧠 实战建议

场景 推荐方案
精确共同好友、数量不大 SINTER / SINTERCARD
只需估算交集数量 PFCOUNT(HyperLogLog)
只需交集数量 + 数据量很大 Bitmap + BITOP + BITCOUNT
数据量超大,追求压缩极致 Roaring Bitmap、Redis Module

如何做一亿用户实时积分排行榜?

做“一亿用户实时积分排行榜”时,面临的挑战是:高并发写入、高频读、排序性能、排行榜分页、内存管理等。使用传统数据库难以胜任,我们通常结合 Redis 的 ZSet(有序集合)结构 + 分布式架构 来高效实现。

💡 核心思路

  • 使用 Redis 的 ZSet 存储用户积分(ZSet天然支持有序集合)。
  • 使用 用户ID为 member,积分为 score,自动排序。
  • 按业务场景设置多个排行榜(总榜、日榜、周榜等)。
  • 使用分片/分区 + 多 Redis 实例缓解内存压力。

🚀 性能优化方案

场景 方案
高并发写入 异步批处理写入积分变化(Kafka / MQ)
高并发读榜 热榜分页结果缓存(Redis 二级缓存)
大数据量 分片存储排行榜(按地域/业务线)
数据持久化 后台定期将 ZSet 持久化至 MySQL(定时备份)
用户查自己排名 排名反查缓存 + 异步修正(定时刷新)

⚙️ 分布式架构下的挑战与解决

问题 解决方案
单机内存不足 Redis 分布式集群 + 按 key 做水平分区
网络波动 Redis 哨兵或主从架构容灾
跨机房 使用 Kafka MQ 异步同步数据
并发冲突 ZINCRBY 原子操作,确保并发安全

内存200M读取1G文件并统计内容重复次数?内存受限时

一次性读取肯定会OOM
可以根据缓冲区分块读取

📌 方案核心:“分治 + 磁盘中间结果 + 再归并统计”


📍第一步:分片预处理(Hash 分桶)

  • 遍历文件,每条记录用 hash 函数映射到 N 个临时小文件中(如 100 个文件)。

  • 例如:

    hash(line) % 100 -> 选择第 i 个 bucket_i.txt 写入
    
  • 每个桶的数据量 << 1GB,避免某一个桶数据过大(可以动态调节桶数)。

📝 实现要点

  • 不能直接把 key 存在内存中,而是用 BufferedWriter 把行写入不同的中间文件。
  • 临时文件名如 bucket_0.txt, bucket_1.txt, …, bucket_99.txt

📍第二步:小文件内统计(Map 阶段)

  • 每个小文件都可以用内存加载(一般几十 MB),然后用 Map<String, Integer> 来做频次统计。

  • 统计完毕后,结果写入新的临时文件,如:

    result_bucket_0.txt:
    word1 -> 5
    word2 -> 3
    ...
    

📍第三步:归并所有桶(Reduce 阶段)

  • 如果需要所有数据的总频率(跨桶汇总),则可以:
    • 对所有结果文件做归并统计(Map 合并)。
    • 比如 word1result_bucket_0.txt 是 5,result_bucket_1.txt 是 2,总共就是 7。
    • 这一步可再次用 hashmap 缓存 + 写磁盘防止内存爆掉。

📘 总结一句话

“把大象装进冰箱,需要分步来——分桶写临时文件 → 每桶局部统计 → 全局归并或提取 Top-N。”

查询200条数据耗时200ms,怎么在500ms内查询1000条数据?

SpringBoot如果有百万数据插入怎么优化?

SpringBoot可以同时处理多少请求?

volatile的应用场景?

为了保证我们并发编程的可见性有序性

SQL的执行流程

单表最多数据量需要多大才涉及到分表?

Mysql引擎层BufferPool工作过程原理?

什么是聚集索引和非聚集索引?

count(*)、count(1)、count(字段) 谁更快?有什么区别?

tb_user
id name
1 潘春尧
2 NULL
3 张三
Ⅰ. SELECT count(*) FROM tb_user;                → 3
Ⅱ. SELECT count(1) FROM tb_user;                → 3
Ⅲ. SELECT count(name) FROM tb_user;             → 2

在功能上没有区别 Ⅲ.如果你统计的数据需要排除NULL 就可以用count(指定字段)
在性能上没有任何区别 非要比较就是count(1)更胜一筹 因为不需要mysql在底层做任何的sql优化

SQL语句中使用了前模糊会导致索引失效?

分库分表id冲突解决方案?

✅ 解决方案详解

1. 数据库主键自增(不推荐)

  • 每个分库或分表自己用数据库的自增主键生成 ID。
  • 缺点:跨库表合并数据时会重复,不具备全局唯一性

2. UUID 作为主键

  • 使用 Java 的 UUID.randomUUID().toString() 等方式。
  • 优点:天然全局唯一
  • 缺点:
    • 不适合做主键(无序、冗长,影响数据库索引性能)。
    • 可读性差,调试困难。

3. 数据库主键段(Segment)模式(推荐)

  • 思路:中心服务维护一张 ID 号段表,为每个业务系统分配一个号段。
  • 实现:
    • 表中记录:biz_tag, max_id, step, version
    • 应用请求号段:SELECT max_id, step FROM id_segment WHERE biz_tag = 'order'
    • 然后更新 max_id 为 max_id + step
  • 优点:
    • 性能高(本地生成,无需每次访问数据库)
    • 避免重复(由号段控制)
  • 缺点:需要一个中心服务(如美团 Leaf)

4. 雪花算法(Snowflake)

  • Twitter 出品,用于生成 64 位整数 ID。

  • 格式如下:

    0 - 41位时间戳 - 10位机器ID - 12位自增序列
    
  • 优点:

    • 单机高性能、趋势递增、可分布式部署
  • 缺点:

    • 依赖机器时钟,系统时间回拨可能导致重复 ID 或服务挂掉
    • 需要保证机器 ID 唯一(通常通过配置或 ZooKeeper 分配)

5. Redis 生成自增 ID

  • 利用 Redis 的 INCR 命令,生成全局递增 ID:

    INCR order:id
    
  • 可配合时间戳、业务前缀等拼接成全局唯一 ID。

  • 优点:

    • 简单、轻量、分布式支持
  • 缺点:

    • Redis 挂掉或主从切换期间可能丢失或重复

🏁 总结推荐

方案 唯一性 性能 实现复杂度 推荐使用场景
UUID 简单 临时标识、测试用途
自增主键 简单 单库表内可用
Segment 中等 企业级 ID 服务(如 Leaf)
雪花算法 中等 分布式高并发业务
Redis INCR 简单 轻量级全局 ID 需求

如果你是在高并发、微服务、分布式环境下,强烈推荐使用:

  • 雪花算法 + Redis 搭配
  • 或引入一个中心 ID 生成服务(如 Leaf、TinyId、美团 UID Generator)

深分页为什么慢,怎么优化?

❓ 问题背景:什么是深分页?

深分页 = 当前页数很大,比如 page=100000 & size=10
对应 SQL:

SELECT * FROM table LIMIT 1000000, 10;
  • LIMIT offset, size 这种分页方式,在 offset 特别大时非常慢
  • 根因:数据库在处理时,仍然会扫描前面 offset 条记录,然后丢弃它们,仅返回后面的 size 条

🐌 为什么慢?

数据库执行过程(如MySQL):

LIMIT 1000000, 10

数据库内部会:

  1. 先从磁盘/缓冲中 取出前 1000000 条记录
  2. 然后只返回第 1000001 ~ 1000010 条;
  3. 前面的全丢了,但依然耗费 CPU、IO 和内存资源。

当数据量大时:

  • IO开销大(全表扫描)
  • CPU开销大(排序、过滤)
  • 数据库响应延迟高

🚀 如何优化深分页?

✅ 方案一:使用 覆盖索引

SELECT id FROM table ORDER BY id LIMIT 1000000, 10;

如果 id 是索引字段,数据库可以直接从索引树上读取,无需回表。

但这个优化能力有限,适合某些查询字段很少、又刚好有索引的情况。


✅ 方案二:记录上次的游标(Keyset Pagination

又叫 基于条件的分页,避免使用 offset。

例如:

SELECT * FROM table 
WHERE id > 上一页最后一条记录id 
ORDER BY id 
LIMIT 10;

优点:

  • 快!数据库利用索引跳过前面的数据
  • 没有 offset,性能非常稳定

适用场景:

  • id 或时间戳等字段是自增或顺序的
  • 不要求用户可以跳到任意页,只支持“向前/向后翻页”

✅ 方案三:缓存 + 异步预处理

对于排行榜、热点数据等:

  • 查询结果提前生成并缓存(Redis 等)
  • 用户点页数 → 直接读缓存,避免实时查询

✅ 方案四:使用临时表或中间结果表

  • 查询结果太大 → 先异步存入临时表
  • 分页从临时表中读取数据(配合游标分页)

适用于复杂 SQL 查询 + 多表连接


✅ 总结对比

方案 优点 缺点 适用场景
offset 分页 简单 深分页慢 小数据量
覆盖索引 限制大 查询字段少
游标分页 性能高 不支持跳页 流式阅读/时间线
缓存分页 一致性差 热点排行榜等
临时表分页 灵活 复杂 大查询分页导出

MySQL的隔离级别实现原理MVCC ?

核心:隐藏字段 + Undo Log + ReadView

🔸 1. 每行记录都有两个隐藏字段:

字段 含义
trx_id(创建事务ID) 表示创建该版本的事务 ID
roll_pointer(回滚指针) 指向 Undo Log(历史版本)的位置

🔸 2. Undo Log(回滚日志)

  • 用于记录数据修改前的旧版本数据
  • 当有读请求(快照读)时,就通过 roll_pointer 访问历史版本
  • 写操作失败或回滚,也依赖 Undo Log 恢复数据

🔸 3. ReadView(读视图)

  • 当事务开启时,会创建一个 ReadView(可见性判断工具)
  • 包含当前活跃事务 ID 列表、最小事务ID、最大事务ID
  • 判断规则:
    • 如果行的创建事务ID < ReadView 中最小事务ID → 可见
    • 如果在活跃事务列表中 → 不可见
    • 如果创建事务ID > 当前事务ID → 不可见(未来数据)
  • **MVCC ** (Multiversion Concurrency Control) 多版本并发控制器

    它是事务隔离级别的无锁的实现方式,用于提高事务的并发性能

事务隔离级别 (isolation)

用来解决并发事务所产生一些问题:
并发:同一个时间,多个线程同时进行请求。
什么时候会发生并发问题:在并发情况下,对同一个数据(变量、对象)进行读写操作才会产生并发问题
并发会产生什么问题?
1.脏读一读已提交(行锁,读不会加锁)
2.不可重复度–重复读(行锁,读和写都会上锁)
3.幻影读–串行化(表锁)概念: 通过设置隔离级别可解决在并发过程中产生的那些问题

用户忘记密码,系统为什么不直接提供原密码,而让改密码

因为系统它也不知道我们的原密码是什么

服务端在保存密码的时候绝不会明文存到数据库,怕有数据库权限的人或者黑客恶意利用
必须用不可逆的加密算法

MD5
  • 只能加密不可解密 但是它是hash算法 可能会有哈希冲突 至少加密2^128次方才有可能发生哈希碰撞
  • 每次生成的密文都一样,不管加密多少次生成的密文都是一样的 可以通过暴力破解
    • 解决暴力破解 就要在里面加 每次加密 解密 都要加入
HS256
  • 增加加密字符长度 目前没有碰撞性
  • 最好加入随机盐
BCrypt → 加入spring-security-core依赖
  • 盐是随机的
  • 无法通过暴力破解

Git怎么修复线上的突发BUG线上突发Bug要修复,本地正在开发新需求

在git里我们通常会用一个单独的分支来进行管理
本地开发也会有一个单独的分支
可以将线上代码的分支签出来单独进行修复

  • 正在开发的代码 → 暂存dev分支
  • 严重故障:回滚上一个版本
    非严重故障:在fix分支修复紧急Bug
  • 非常紧急:直接合并master分支上线
  • 一般急:合并release分支,走测试、上线流程
  • 非紧急:合并dev分支,走测试、线上流程

RestTemplate如何优化连接池

默认是没有连接池的
要用框架HTTPClientOKHTTP

RestTemplate 是Spring框架中用于简化HTTP请求的一个类,它提供了多种方法来处理HTTP请求和响应。RestTemplate可以用于发送GET、POST、PUT、DELETE等HTTP请求,并且可以处理请求头、请求体、URL参数等。

Synchronized怎么提升性能

🧠 实际开发中如何用好 synchronized?

✅ 尽量减少锁的粒度

// ❌ 锁了整个方法
public synchronized void update() { ... }

// ✅ 只锁关键代码段
public void update() {
    // 非关键代码
    synchronized (this) {
        // 只锁关键部分
    }
}

✅ 使用局部锁对象代替类锁

private final Object lock = new Object();

public void doTask() {
    synchronized (lock) {
        // 更细粒度的锁,避免不必要的争用
    }
}

✅ 使用并发类替代锁(性能更高)

  • ConcurrentHashMap
  • ReadWriteLock
  • ReentrantLock
  • AtomicInteger

开发中有没有用到设计模式?怎么用的

策略模式 + 简单工厂 + 模板方法

SpringBoot如何防止反编译

有没有出现过Spring正常SpringBoot报错的情况?

SpringBoot配置文件敏感信息如何加密?

一个需求来了怎么办
首先看这个需求 进行一个分析 分析这个需求跟哪些功能有关联 比如说在我做过的xxx里面 和什么关联 要思考怎么去做这个关联 数据库 代码层面 思考好之后 再去ER画图 写接口文档 再去开始写代码

阅读全文

SpringBoot趣味实战课

2024/10/21

Swagger + Mariadb + Hibernate 实现极简CRUD

application.yaml
spring:
  application:
    name: Pluminary
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/pcy?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&nullCatalogMeansCurrent=true
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MariaDB103Dialect
  springdoc:
    api-docs:
      path: /v3/api-docs
    swagger-ui:
      path: /swagger-ui.html

  server:
    port: 8080
    servlet:
      context-path: /springboot
      session:
        timeout: 60
  debug: true
com/pcy/Swagger/SwaggerConfig.java
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**") //这里是扫描包
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
com/pcy/service/UserRepository.java //【这个是持久化接口 实现CRUD】
package com.pcy.service;

import com.pcy.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Integer> {
}
com/pcy/controller/UserController.java
package com.pcy.controller;

import com.pcy.dao.User;
import com.pcy.service.UserRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
@Tag(name = "User Controller", description = "用户相关操作")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功获取用户信息"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @GetMapping("/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }

    @Operation(summary = "创建用户", description = "创建一个新的用户")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功创建用户"),
            @ApiResponse(responseCode = "400", description = "无效的输入")
    })
    @PostMapping
    public User create(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "更新用户", description = "更新用户信息")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功更新用户信息"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @PutMapping
    public User update(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "删除用户", description = "根据ID删除用户")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功删除用户"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @DeleteMapping("/{id}")
    public void delete(@PathVariable int id) {
        userRepository.deleteById(id);
    }
}
pom.xml
<?xml version="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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pcy</groupId>
    <artifactId>Pluminary</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Pluminary</name>
    <description>Pluminary</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>mysql</groupId>-->
<!--            <artifactId>mysql-connector-java</artifactId>-->
<!--            <version>8.0.33</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>2.7.4</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.1.7.Final</version> <!-- 选择与 Spring Boot 3.3.2 兼容的版本 -->
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

增加分页、排序

com/pcy/controller/UserController.java
@Operation(summary = "获取用户列表", description = "获取用户列表")
    @GetMapping
    public Page<User> list(@RequestParam(defaultValue = "id") String property,
 @RequestParam(defaultValue = "ASC")Sort.Direction direction,
 @RequestParam(defaultValue = "0") Integer page,
 @RequestParam(defaultValue = "10") Integer pageSize) {
    Pageable pageable = PageRequest.of(page, pageSize, direction, property);
        return userRepository.findAll(pageable);
    }

根据姓名查用户

com/pcy/controller/UserController.java    
    @Operation(summary = "根据姓名查用户",description = "根据姓名查用户")
    @GetMapping("/name")
    public List<User> getByName(String name){
        return userRepository.findByNameContaining(name);
    }
com/pcy/service/UserRepository.java
package com.pcy.service;

import com.pcy.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface UserRepository extends JpaRepository<User,Integer> {
    List<User> findByNameContaining(String name);
}

根据生日查用户、删除User表

com/pcy/controller/UserController.java
@Operation(summary = "根据生日获取用户信息①",description = "根据生日获取用户信息①")
    @GetMapping("/birthdayOne")
    public List<User> getBirthDayOne(LocalDate birthDay){
        return userRepository.findByBirthDay(birthDay);
    }

    @Operation(summary = "根据生日获取用户信息②",description = "根据生日获取用户信息②")
    @GetMapping("/birthdayTwo")
    public List<User> getBirthDayTwo(LocalDate birthDay){
        return userRepository.findByBirthDayNative(birthDay);
    }

    @Operation(summary = "删除User",description = "删除User")
    @GetMapping("/delete")
    public void delete(){
        userRepository.delete();
    }
com/pcy/service/UserRepository.java
@Query("SELECT u FROM User u WHERE u.birthday=?1")
    List<User> findByBirthDay(LocalDate birthDay);

    @Query(value = "SELECT * FROM user WHERE birth_day =:birthDay",nativeQuery = true)
    List<User> findByBirthDayNative(LocalDate birthDay);

    @Modifying
    @Transactional
    @Query(value = "DELETE FROM User")
    int delete();

增加审计

com/pcy/MallApplication.java //【增加@EnableJpaAuditing】
package com.pcy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}
com/pcy/dao/BaseEntity.java //【没有必要为每个实体类都编写 直接封装导一个类 User去继承】
package com.pcy.dao;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Data
@MappedSuperclass
//该注解用于监听实体类,在save、update之后的状态
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
    @CreatedBy
    @Column(updatable = false)
    private String creator;

    @LastModifiedBy
    private String modifier;

    @CreatedDate
    @Column(updatable = false) //不可修改的
    private LocalDateTime createTime;

    @LastModifiedDate
    private LocalDateTime updateTime;
}
com/pcy/dao/User.java //【增加@EqualsAndHashCode 与 extends BaseEntity】
@Data
@Entity
@EqualsAndHashCode(callSuper = true)
//@Schema(name="用户信息")
@Table(indexes = {@Index(name = "uk_email",columnList = "email",unique = true)})
public class User extends BaseEntity{
    @Id
//    @Schema(description = "用户ID")
//    @NotBlank(message = "Id不能为空")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    ...
}
com/pcy/service/impl/AuditorAwareImpl.java
package com.pcy.service.impl;

import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        // 添加一个随机数
        return Optional.of("管理员"+(int)(Math.random()));
    }
}

引入Mybatis-Plus + FreeMarker

pom.xml
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.4.2</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
//根据你提供的实体类BaseEntity和User,我为你设计了一个基于MyBatis-Plus 3.5.x版本的代码生成器MysqlGenerator,它将自动生成与这些实体类相关的代码,如Mapper、Service、Controller等。以下是生成器的代码示例
【仅供查看学习 实际代码爆红无法导入】
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.builder.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.fill.Property;
import com.baomidou.mybatisplus.generator.keywords.MySqlKeyWordsHandler;

import java.util.Collections;

public class MysqlGenerator {

    // 项目路径
    private static final String PROJECT_PATH = System.getProperty("user.dir");
    // 输出路径
    private static final String OUTPUT_DIR = PROJECT_PATH + "/src/main/java";
    // 作者
    private static final String AUTHOR = "YourName";
    // 包名
    private static final String BASE_PACKAGE = "com.pcy";
    // 数据源配置
    private static final String DATABASE_URL = "jdbc:mysql://localhost:3306/your_database";
    private static final String DATABASE_USERNAME = "root";
    private static final String DATABASE_PASSWORD = "password";
    private static final String DATABASE_DRIVER = "com.mysql.cj.jdbc.Driver";

    public static void main(String[] args) {
        // 1. 全局配置
        GlobalConfig.Builder globalConfig = new GlobalConfig.Builder()
            .outputDir(OUTPUT_DIR)
            .author(AUTHOR)
            .enableSwagger()
            .fileOverride()
            .disableOpenDir(); // 不自动打开输出目录

        // 2. 数据源配置
        DataSourceConfig.Builder dataSourceConfig = new DataSourceConfig.Builder(DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD)
            .dbQuery(new MySqlQuery())
            .schema("public")
            .dbType(DbType.MYSQL)
            .keyWordsHandler(new MySqlKeyWordsHandler())
            .driverName(DATABASE_DRIVER);

        // 3. 包配置
        PackageConfig.Builder packageConfig = new PackageConfig.Builder()
            .parent(BASE_PACKAGE)
            .entity("dao")
            .mapper("mapper")
            .service("service")
            .controller("controller");

        // 4. 策略配置
        StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder()
            .addInclude("user") // 生成指定表
            .addTablePrefix("t_") // 去掉表前缀
            .entityBuilder()
                .superClass(BaseEntity.class)
                .enableLombok()
                .addSuperEntityColumns("id", "creator", "modifier", "create_time", "update_time")
                .logicDeleteColumnName("deleted")
                .addTableFills(new Property("create_time", FieldFill.INSERT))
                .addTableFills(new Property("update_time", FieldFill.INSERT_UPDATE))
                .enableActiveRecord()
                .naming(NamingStrategy.underline_to_camel)
                .columnNaming(NamingStrategy.underline_to_camel)
            .controllerBuilder()
                .enableRestStyle()
                .enableHyphenStyle()
            .serviceBuilder()
                .formatServiceFileName("%sService")
                .formatServiceImplFileName("%sServiceImpl")
            .mapperBuilder()
                .enableBaseResultMap()
                .enableBaseColumnList();

        // 5. 模板配置
        TemplateConfig.Builder templateConfig = new TemplateConfig.Builder();

        // 6. 自定义配置
        InjectionConfig.Builder injectionConfig = new InjectionConfig.Builder()
            .beforeOutputFile((tableInfo, objectMap) -> objectMap.put("parent", BASE_PACKAGE));

        // 7. 整合配置
        AutoGenerator autoGenerator = new AutoGenerator(dataSourceConfig.build())
            .global(globalConfig.build())
            .packageInfo(packageConfig.build())
            .strategy(strategyConfig.build())
            .template(templateConfig.build())
            .injection(injectionConfig.build())
            .templateEngine(new FreemarkerTemplateEngine()); // 选择模板引擎

        // 8. 执行
        autoGenerator.execute();
    }
}
/*
关键配置说明:
GlobalConfig:设置代码生成的全局配置,包括作者、输出目录、是否覆盖已有文件等。
DataSourceConfig:配置数据库连接信息,使用MySQL数据库。
PackageConfig:指定生成的代码所在的包路径。
StrategyConfig:配置生成策略,包括实体类的继承关系、使用Lombok、Rest风格的控制器等。
TemplateConfig:模板配置,可定制生成的模板。
InjectionConfig:自定义配置,用于在生成文件前注入自定义的变量或逻辑。
AutoGenerator:整合所有配置并执行代码生成。

生成的文件包括:
实体类:根据数据库表生成实体类,并继承BaseEntity。
Mapper接口:生成Mapper接口用于数据库操作。
Service接口和实现类:生成Service接口及其实现类。
Controller类:生成Rest风格的控制器类。

使用方法:
修改数据库连接信息(DATABASE_URL、DATABASE_USERNAME、DATABASE_PASSWORD)。
配置需要生成代码的表名(addInclude("user"))。
运行MysqlGenerator.java的main方法,代码将会生成在指定的输出目录中。
*
//【以下都是自动生成的代码】
com/pcy/mapper/UserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
com/pcy/service/UserService.java
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.User;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * 用户表 服务类
 * </p>
 */
public interface UserService extends IService<User> {
// 在Spring中使用事务
    @Transactional(propagation = Propagation.REQUIRED)
    void addWithRequired(User user);

    @Transactional(propagation = Propagation.REQUIRED)
    void addWithRequiredAndException(User user);

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void addWithRequiredNew(User user);

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void addWithRequiredNewAndException(User user);

    @Transactional(propagation = Propagation.NESTED)
    void addWithNested(User user);

    @Transactional(propagation = Propagation.NESTED)
    void addWithNestedAndException(User user);
}
com/pcy/service/impl/UserServiceImpl.java
package com.pcy.service.impl;


import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pcy.entity.User;
import com.pcy.mapper.UserMapper;
import com.pcy.service.UserService;
import com.pcy.mapper.UserMapper;
import com.pcy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private UserMapper mapper;
    
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addWithRequired(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addWithRequiredAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addWithRequiredNew(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addWithRequiredNewAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }

    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addWithNested(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addWithNestedAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }
}
resources/mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pcy.mapper.UserMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.pcy.entity.User">
    <result column="id" property="id" />
    <result column="creator" property="creator" />
    <result column="modifier" property="modifier" />
    <result column="create_time" property="createTime" />
    <result column="update_time" property="updateTime" />
        <result column="name" property="name" />
        <result column="email" property="email" />
        <result column="birth_day" property="birthDay" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id,
        creator,
        modifier,
        create_time,
        update_time,
        name, email, birth_day
    </sql>

</mapper>
//【提问:爆bug  "Could not autowire. No beans of 'UserMapper' type found"】 深度解析
1. @MapperScan 注解的原理 //启动类里面的 @MapperScan("com.pcy.mapper")
@MapperScan 是 MyBatis-Spring 提供的一个注解,用于指定要扫描的 Mapper 接口所在的包路径。它的作用是告诉 Spring 框架应该在哪些包路径下寻找 Mapper 接口,并将它们注册为 Spring 的 Bean。
扫描 Mapper 接口:Spring Boot 在启动时,会扫描你指定的包路径下的所有接口,并检测这些接口是否包含 MyBatis 的 Mapper 注解或者继承了 BaseMapper 等相关接口。
注册为 Bean:一旦找到这些接口,Spring 会自动为这些接口生成一个实现类,并将它们注册为 Spring 容器中的 Bean,这样你就可以通过 @Autowired 注入这些 Mapper。

2. @Mapper 注解的原理
@Mapper 是 MyBatis 提供的一个注解,用于标记一个接口为 MyBatis 的 Mapper 接口。被标记为 @Mapper 的接口会被 MyBatis-Spring 扫描到,并且 MyBatis 会为该接口生成一个实现类,负责执行 SQL 语句。
当你在 UserMapper 接口上添加 @Mapper 注解时,即使没有使用 @MapperScan,MyBatis 也会知道这个接口是一个 Mapper 接口,并将其注册为一个 Bean。这使得你可以在 UserServiceImpl 中通过 @Autowired 注入它。

3. 为什么使用 @MapperScan 和 @Mapper 不会报错
自动注册 Bean:@MapperScan 会自动扫描指定包路径下的所有 Mapper 接口,并将它们注册为 Spring 容器中的 Bean。这意味着在 UserServiceImpl 中,当你使用 @Autowired 注入 UserMapper 时,Spring 可以找到对应的 Bean,从而避免 Could not autowire 错误。
手动注册 Bean:当你在 Mapper 接口上直接使用 @Mapper 注解时,Spring 也会将该接口注册为一个 Bean,这样你同样可以通过 @Autowired 进行注入,而不会出现 Bean 找不到的问题。
//【提问:MysqlGenerator 逆向生成那些包的原理】
MyBatis-Plus 提供的 MyBatis-Plus Generator 是一个非常强大的代码生成工具,可以通过数据库表结构生成对应的 Java 代码,包括实体类、Mapper 接口、Mapper XML 文件、Service 类、Controller 类等。这个过程通常被称为“逆向工程”或“代码生成”。
1. MyBatis-Plus Generator 的工作原理
 1.1 读取数据库表结构
数据源配置:首先,MyBatis-Plus Generator 通过配置的数据源连接到指定的数据库。它会读取数据库中的表结构信息,包括表名、字段名、数据类型、主键、外键、索引等信息。
> DataSourceConfig dsc = new DataSourceConfig.Builder(DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD)
    .driverName(DATABASE_DRIVER)
    .build();

元数据解析:MyBatis-Plus Generator 通过 JDBC 获取数据库的元数据 (Metadata),并解析每个表的结构,将其转换为可以用于代码生成的数据结构。

 1.2 生成代码
代码生成器:AutoGenerator 是核心的代码生成器类。它根据从数据库中获取的表结构信息,生成相应的 Java 类文件。
> AutoGenerator generator = new AutoGenerator(dsc);

模板引擎:MyBatis-Plus Generator 使用模板引擎(例如 Freemarker)来渲染代码模板。通过模板和解析后的元数据,生成代码文件。每个生成的 Java 类文件都对应着一个模板文件,模板文件中包含了如何生成特定类型文件的逻辑。
> generator.templateEngine(new FreemarkerTemplateEngine());

 1.3 生成的包和文件
实体类 (entity):根据表结构生成对应的 Java 实体类。每个实体类与数据库表一一对应,包含表中字段的定义。
> strategyConfig.entityBuilder().enableLombok().naming(NamingStrategy.underline_to_camel);

Mapper 接口 (mapper):生成的 Mapper 接口用于与数据库交互,执行基本的增删改查操作。Mapper 接口通常继承自 BaseMapper,提供基本的 CRUD 操作。
> strategyConfig.mapperBuilder().enableBaseResultMap().enableBaseColumnList();
Mapper XML 文件 (mapper.xml):生成的 Mapper XML 文件包含了 Mapper 接口中对应的方法的 SQL 语句。这些 XML 文件用于定义复杂的查询、更新语句等。

Service 接口和实现类 (service, service.impl):Service 层是业务逻辑层。生成的 Service 接口提供了业务操作的定义,Service 实现类则实现这些业务操作。
> strategyConfig.serviceBuilder().formatServiceFileName("%sService");

Controller 类 (controller):生成的 Controller 类用于处理 HTTP 请求,调用 Service 层的方法进行业务处理,然后返回结果。Controller 通常与前端交互,处理用户请求。
> strategyConfig.controllerBuilder().enableRestStyle().enableHyphenStyle();


2. MyBatis-Plus Generator 如何生成这些包和文件
 2.1 代码生成策略 (StrategyConfig)
StrategyConfig 类用于配置代码生成的策略,如生成哪些表,生成哪些类,类的命名规则,是否使用 Lombok 等。
StrategyConfig strategyConfig = new StrategyConfig.Builder()
    .addInclude("user") // 生成指定表
    .entityBuilder().enableLombok() // 实体类配置
    .mapperBuilder().enableBaseResultMap() // Mapper 配置
    .serviceBuilder().formatServiceFileName("%sService") // Service 配置
    .controllerBuilder().enableRestStyle() // Controller 配置
    .build();

 2.2 模板文件
MyBatis-Plus Generator 使用的模板文件可以自定义,通常位于 resources/templates 目录下。每个模板文件对应一个需要生成的 Java 文件类型,例如 entity.java.ftl 对应实体类,mapper.java.ftl 对应 Mapper 接口。
     
模板文件中可以使用变量和逻辑来决定生成的代码内容。例如,${className} 会被替换为实际的类名,<#if useLombok> @Data </#if> 会根据条件生成代码。
     
 2.3 文件输出配置 (InjectionConfig 和 FileOutConfig)
通过 InjectionConfig 和 FileOutConfig,可以控制生成文件的路径、名称、以及自定义生成的文件内容。例如,可以指定某个表的实体类生成到特定的包下,或者将 XML 文件输出到特定的路径。
InjectionConfig cfg = new InjectionConfig.Builder()
    .beforeOutputFile((tableInfo, objectMap) -> {
        // 自定义处理逻辑
    })
    .build();

用MyBatis Plus的分页

pom.xml
<?xml version="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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pcy</groupId>
    <artifactId>Pluminary</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Pluminary</name>
    <description>Pluminary</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>mysql</groupId>-->
<!--            <artifactId>mysql-connector-java</artifactId>-->
<!--            <version>8.0.33</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.1.7.Final</version> <!-- 选择与 Spring Boot 3.3.2 兼容的版本 -->
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
            <exclusions>
                <exclusion>
                    <artifactId>mybatis-spring</artifactId>
                    <groupId>org.mybatis</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.5</version> <!-- 版本对齐 -->
        </dependency>

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

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
//【添加MyBatis-Plus的分页插件】
com/pcy/utils/MyBatisPlusConfig.java
package com.pcy.utils;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//@Configuration 用于定义配置类,被注解的类内部包含有一个或多个被@Bean注解的方法
// 用于构建bean定义,初始化Spring容器
@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MARIADB));
        return interceptor;
    }
}
com/pcy/controller/UserController.java //【增加listPage】
package com.pcy.controller;

import com.pcy.entity.User;
import com.pcy.service.UserRepository;
import com.pcy.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.List;

@RestController
@RequestMapping("/users")
@Tag(name = "User Controller", description = "用户相关操作")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @GetMapping("/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }

    @Operation(summary = "创建用户", description = "创建一个新的用户")
    @PostMapping
    public User create(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "更新用户", description = "更新用户信息")
    @PutMapping
    public User update(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "删除用户", description = "根据ID删除用户")
    @DeleteMapping("/{id}")
    public void delete(@PathVariable int id) {
        userRepository.deleteById(id);
    }

    @Operation(summary = "获取用户列表", description = "获取用户列表")
    @GetMapping("/list")
    public org.springframework.data.domain.Page<User> list(@RequestParam(defaultValue = "id") String property,
                                                           @RequestParam(defaultValue = "ASC") Sort.Direction direction,
                                                           @RequestParam(defaultValue = "0") Integer page,
                                                           @RequestParam(defaultValue = "10") Integer pageSize) {
        Pageable pageable = PageRequest.of(page, pageSize, direction, property);
        return userRepository.findAll(pageable);
    }

    @Operation(summary = "根据生日获取用户信息①", description = "根据生日获取用户信息①")
    @GetMapping("/birthdayOne")
    public List<User> getBirthDayOne(@RequestParam LocalDate birthDay) {
        return userRepository.findByBirthDay(birthDay);
    }

    @Operation(summary = "根据生日获取用户信息②", description = "根据生日获取用户信息②")
    @GetMapping("/birthdayTwo")
    public List<User> getBirthDayTwo(@RequestParam LocalDate birthDay) {
        return userRepository.findByBirthDayNative(birthDay);
    }

    @Operation(summary = "删除所有用户", description = "删除所有用户")
    @DeleteMapping("/deleteAll")
    public void deleteAll() {
        userRepository.deleteAll();
    }

    @Operation(summary = "分页查询用户列表", description = "分页查询用户列表")
    @GetMapping("/page")
    public Page<User> listPage(@RequestParam(defaultValue = "1") Integer page,
                               @RequestParam(defaultValue = "10") Integer pageSize) {
        return userService.page(new Page<>(page, pageSize));
    }
}

高级SQL语句(Lambda)

wrapper.lambda().like(user -> user.getName(), "p");
/*
Lambda 表达式:

user -> user.getName() 是一个 Lambda 表达式。
user 是 User 类的一个实例,作为 Lambda 表达式的输入参数。
user.getName() 是对 user 对象的 getName() 方法的调用,返回 name 字段的值。
作用:

这行代码告诉 MyBatis-Plus:在生成的 SQL 查询中,查找 name 字段值中包含 "p" 的所有记录。
wrapper.lambda() 返回一个 LambdaQueryWrapper<User> 对象,支持使用 Lambda 表达式进行条件构建。
.like() 方法添加了一个 LIKE 条件,表示在 SQL 查询中进行模糊匹配。
*/


wrapper.lambda().like(User::getName, "p");
/*
方法引用:

User::getName 是一种方法引用,它引用了 User 类的 getName() 方法。
方法引用是对 Lambda 表达式的一种简写。它表示将某个方法作为函数式接口的实现。
作用:

这行代码与第一行代码的作用相同,都是在生成的 SQL 查询中查找 name 字段值中包含 "p" 的所有记录。
User::getName 告诉 MyBatis-Plus:使用 User 类中的 getName() 方法来获取要参与条件判断的字段。
*/
com/pcy/controller/UserController.java
@Operation(summary = "自定义查询", description = "自定义查询")
    @GetMapping("/Dingyi")
    public List<User> getWrapper() { //类型List<User> 可以返回数据库列表
        QueryWrapper<User> wrapper = new QueryWrapper<>();
//        wrapper.eq("name", "潘春尧");
//        wrapper.lambda().ge(User::getBirthDay, LocalDate.parse("2011-01-01"));
//        wrapper.between(User::getBirthDay, "2011-01-01", "2011-12-31");
        wrapper.lambda().like(User::getName, "string");

//      wrapper.lambda().like(user -> user.getName(), "p");
//        wrapper.select("name,count(*)").groupBy("name");
//        return (QueryWrapper<User>) userMapper.selectList(wrapper);
//        wrapper.in(CollectionUtils.isNotEmpty(nameList), User::getName, nameList);
        return userMapper.selectList(wrapper);
    }

自动填充、填充实现策略

com/pcy/utils/MyMetaObjectHandler.java
package com.pcy.utils;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;

import java.time.LocalDateTime;

public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "creator", this::getCurrentUser, String.class);
        this.strictInsertFill(metaObject, "modifier", this::getCurrentUser, String.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
        this.strictUpdateFill(metaObject, "modifier", this::getCurrentUser, String.class);
    }
    
    // 模拟获取当前用户
    private String getCurrentUser(){
        return "管理员" + (int) (Math.random() * 10);
    }
}
// 这是自动填充的原理
default MetaObjectHandler strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) {
        if (metaObject.getValue(fieldName) == null) {
            Object obj = fieldVal.get();
            if (Objects.nonNull(obj)) {
                metaObject.setValue(fieldName, obj);
            }
        }
        return this;
    }

强大的Druid

pom.xml
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
// Druid和MariaDB是两种不同类型的数据库系统
1、类型和用途:
Druid:Druid是一种分布式的实时分析数据库,主要用于处理高吞吐量的时间序列数据或事件数据。它专为快速查询和分析大规模数据而设计,常用于数据仓库、在线分析处理(OLAP)以及实时数据分析等场景。
MariaDB:MariaDB是一种关系型数据库管理系统(RDBMS),它是MySQL的一个分支,广泛用于常规的事务处理、数据存储和管理。MariaDB通常用于传统的OLTP(在线事务处理)场景,如web应用、内容管理系统等。

2、适用场景:
Druid:适合用于实时数据分析、日志分析、时间序列分析、用户行为分析等需要快速响应的场景。
MariaDB:适合传统的数据库应用,如电子商务系统、内容管理系统、ERP、CRM等需要强事务处理能力的场景。
    
总结来说,Druid和MariaDB各自适用于不同的数据处理需求,Druid更侧重于实时分析和大规模数据处理,而MariaDB更侧重于事务处理和关系型数据管理

Spring Data JPA与MyBatis-Plus的区别并且简单举例说明

Spring Data JPA: //【实现接口】

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // Other fields, getters, and setters
}

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);
}
Spring Data JPA: //【实现控制类】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/jpa/users")
public class UserJpaController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
                             .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        return userRepository.save(user);
    }
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User user = userRepository.findById(id)
                                  .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));

        user.setName(userDetails.getName());
        // Update other fields here
        return userRepository.save(user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        User user = userRepository.findById(id)
                                  .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));

        userRepository.delete(user);
    }

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        return userRepository.findByName(name);
    }
}


MyBatis Plus: //【实现接口】

@TableName("user")
public class User {
    private Long id;
    private String name;
    // Other fields, getters, and setters
}

public interface UserMapper extends BaseMapper<User> {
    // Custom SQL
    @Select("SELECT * FROM user WHERE name = #{name}")
    List<User> selectByName(@Param("name") String name);
}
MyBatis-Plus: //【实现控制类】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/mybatis/users")
public class UserMyBatisController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping
    public List<User> getAllUsers() {
        return userMapper.selectList(null);
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userMapper.selectById(id);
    }

    @PostMapping
    public void createUser(@RequestBody User user) {
        userMapper.insert(user);
    }

    @PutMapping("/{id}")
    public void updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new ResourceNotFoundException("User not found with id: " + id);
        }

        user.setName(userDetails.getName());
        // Update other fields here
        userMapper.updateById(user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new ResourceNotFoundException("User not found with id: " + id);
        }

        userMapper.deleteById(id);
    }

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        return userMapper.selectByName(name);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// 构建查询条件的包装类,它使用 Lambda 表达式避免了手写字符串可能导致的字段错误。
// 这种方式非常适合需要根据多个条件动态生成SQL查询的场景,使用LambdaQueryWrapper不仅能提高代码的可读性,还能减少由于硬编码字符串导致的错误。
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/mybatis/users")
public class UserMyBatisController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        // 使用 LambdaQueryWrapper 构建模糊查询条件
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(User::getName, name); // 类似于 SQL 中的 "WHERE name LIKE '%name%'"

        // 执行查询并返回结果
        return userMapper.selectList(queryWrapper);
    }
    // 其他CRUD方法与前面的示例相同
}

Junit

经过单元测试,观察日志输出,就会发现没有进行数据库查询,对数据库的交互逻辑不是Service层的单元测试需要关心的事情,而是Dao层的单元测试需要考虑的。Service层的单元测试是假定Dao层全部正确的基础上写的,我们只需要关注Service层是正确即可。
pom.xml
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
com/pcy/service/impl/UserServiceImpl.java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    @Autowired
    private UserMapper mapper;

    public User getById(int id) {
        logger.info("id为:",id);
        return mapper.selectById(id);
    }
......
}
这是测试Service
test/java  com/pcy/service/impl/UserServiceImplTest.java //【用Mock改造 + log4j】
// 检查 UserServiceImpl 是否在测试中被 @MockBean 或其他方式替换为Mock对象。如果使用了Mock对象,测试时不会真正访问数据库,而是使用模拟数据。
package com.pcy.service.impl;

import com.pcy.entity.User;
import com.pcy.mapper.UserMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class UserServiceImplTest {
    @InjectMocks
    UserServiceImpl userService;

    @Mock
    UserMapper userMapper;

    @Test
    @DisplayName("Test Service getById")
    void getById() {
        // 模拟userMapper的selectById方法返回一个User对象
        User mockUser = new User().setId(1).setName("qwe").setEmail("1234@qq.com");
        Mockito.when(userMapper.selectById(1)).thenReturn(mockUser);

        // 调用userService的getById方法,并验证返回结果
        User user = userService.getById(1);

        System.out.println(user);
        Assertions.assertEquals("qwe", user.getName());
    }
}
=====================================================================
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-08-12T21:18:07.805+08:00  INFO 31512 --- [Pluminary] [           main] com.pcy.service.impl.UserServiceImpl     : id为:
User(id=1, name=qwe, age=0, email=1234@qq.com, birthDay=null)
com/pcy/entity/User.java
//你的 User 类同时使用了 Lombok 注解 (@Data, @Accessors(chain = true)) 和手动定义的 getter/setter 方法。由于 Lombok 已经生成了这些方法,手动定义的 getter/setter 方法会覆盖 Lombok 自动生成的方法,这可能导致链式调用的 setEmail 和其他类似方法无法正确解析。
package com.pcy.entity;

import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.time.LocalDate;

@Data
@Entity
@EqualsAndHashCode(callSuper = true)
//@Schema(name="用户信息")
@Table(indexes = {@Index(name = "uk_email",columnList = "email",unique = true)})
@Accessors(chain = true) // 允许链式调用
public class User extends BaseEntity{
    @Id
//    @Schema(description = "用户ID")
//    @NotBlank(message = "Id不能为空")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

//    @Schema(description = "用字")
//    @NotBlank(message = "名字不能为空")
    @Column(nullable = false, columnDefinition = "varchar(20) comment '姓名'")
    private String name;

//    @Transient //注解修饰
//    @Schema(description = "年龄")
//    @Min(value = 1, message = "年龄不能小于1")
    private int age;

//    @Schema(description = "邮箱")
//    @Email(message = "E-mail格式不正确")
    @Column(nullable = false, length = 50)
    private String email;

//    @Schema(description = "生日")
//    @Past(message = "生日必须为过去的时间")
    private LocalDate birthDay;
}
这是测试Controller
test/java  com/pcy/controller/UserControllerTest.java
// Controller层的单元测试需要用到一个特定的类——MockMvc 专门为SpringMVC提供支持的
package com.pcy.controller;

import com.pcy.entity.User;
import com.pcy.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@SpringBootTest
class UserControllerTest {
    MockMvc mockMvc;

    @Mock
    UserService userService;

    @InjectMocks
    UserController userController;

    @BeforeEach
    void setUp(){
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
    }

    @Test
    @DisplayName("Test Controller get")
    void get() throws Exception {
        Mockito.when(userService.getById(1)).thenReturn(new User().setName("刘水镜").setEmail("liushuijing@mail.com"));
        BDDMockito.given(userService.getById(1)).willReturn(new User().setName("刘水镜").setEmail("liushuijing@mail.com"));
        mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)
                        .accept("application/json;charset=UTF-8")
                        .contentType("application/json;charset=UTF-8"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("刘水镜"))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        log.info("Test Controller get");
    }
}

全局异常处理

/*
一、@RestControllerAdvice 注解的作用
@RestControllerAdvice 是 Spring Framework 为我们提供的一个复合注解,它是 @ControllerAdvice 和 @ResponseBody 的结合体。

@ControllerAdvice:该注解标志着一个类可以为所有的 @RequestMapping 处理方法提供通用的异常处理和数据绑定等增强功能。当应用到一个类上时,该类中定义的方法将在所有控制器类的请求处理链中生效。

@ResponseBody:表示方法的返回值将被直接写入 HTTP 响应体中,通常配合 Jackson 或 Gson 等 JSON 库将对象转换为 JSON 格式的响应。

因此,@RestControllerAdvice 就是专门为 RESTful 控制器设计的全局异常处理器,它的方法返回值将自动转换为响应体。
*/
“全球”异常
com/pcy/controller/UserController.java
@Operation(summary = "异常查询", description = "异常查询")
    @GetMapping(value = "/{id}")
    public Result<User> get(@PathVariable Integer id) {
        User user = userService.getById(id);
        if (user == null){
            throw new RuntimeException("找不到id信息" + id);
        }
        return Result.success(userService.getById(id));
    }
/*
当输入id信息错误的时候
{
  "code": 200,
  "message": "操作成功",
  "data": {
    "creator": null,
    "modifier": null,
    "createTime": null,
    "updateTime": null,
    "id": 1,
    "name": "潘春尧",
    "age": 1,
    "email": "390@qq.com",
    "birthDay": "2024-08-10"
  }
}


当输入id信息错误的时候
{
  "code": 500,
  "message": "找不到id信息3323",
  "data": null
}
*/
com/pcy/utils/GlobalExceptionHandler.java
package com.pcy.utils;

import com.pcy.entity.MessageEnum;
import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//@ExceptionHandler注解用于在Spring MVC控制器中处理特定类型的异常。它可以应用于方法上
//当控制器方法抛出指定类型的异常时,@ExceptionHandler注解的方法将被调用来处理该异常
    @ExceptionHandler(Exception.class)
    public Result<Boolean> globalException(Exception e){
        Result<Boolean> result = new Result<>();
        result.setCode(MessageEnum.ERROR.getCode());
        result.setMessage(e.getMessage() == null ? MessageEnum.ERROR.getMessage() : e.getMessage());
        log.error(e.getMessage(), e);
        return result;
    }
}
com/pcy/entity/MessageEnum.java
package com.pcy.entity;

import lombok.Getter;

@Getter
public enum MessageEnum {
    SUCCESS(200, "操作成功"),
    ERROR(500, "操作失败");

    private final Integer code;
    private final String message;
    MessageEnum(Integer code, String message){
        this.code = code;
        this.message = message;
    }
}
com/pcy/entity/Result.java
package com.pcy.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
   private Integer code;
   private String message;
   private T data;
   // 用于生成一个没有具体数据内容的成功响应
   public static <T> Result<T> success(){
       return success(null);
   }
   // 用于生成包含数据的成功响应
    public static <T> Result<T> success(T data){
        return new Result<>(MessageEnum.SUCCESS.getCode(), MessageEnum.SUCCESS.getMessage(), data);
    }
    // 用于生成一个没有具体错误信息的默认错误响应
    public static<T> Result<T> error(){
        return error(MessageEnum.ERROR);
    }
    // 用于生成带有特定错误信息的错误响应,MessageEnum 是一个枚举类型,包含了不同的错误信息和代码。
    public static<T> Result<T> error(MessageEnum messageEnum){
        return new Result<>(messageEnum.ERROR.getCode(), messageEnum.getMessage(), null);
    }
    // 用于生成包含自定义错误信息的错误响应
    public static <T> Result<T> error(String message) {
        return error(message, MessageEnum.ERROR.getCode());
    }
    // 用于生成包含自定义错误信息和自定义状态码的错误响应
    protected static <T> Result<T> error(String message, Integer code) {
        return new Result<>(code, message, null);
    }
}
写个小异常
com/pcy/controller/ExceptionController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/exception")
@Tag(name = "Exception", description = "异常操作")
public class ExceptionController {
    @GetMapping("/runtimeexception")
    public Result<Boolean> runtimeException(){
        throw new RuntimeException();
    }
}
/*
开启全局异常处理的返回值
{
  "code": 500,
  "message": "操作失败",
  "data": null
}

没有全局异常处理的错误返回值
{
  "timestamp": "2024-08-13T08:21:43.192+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/exception/runtimeexception"
}
*/
//在SwaggerConfig中添加扫描路径 "/exception/**"  不然接口无法获取
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// http://localhost:8080/swagger-ui/index.html
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
你提到的 GlobalExceptionHandler 和 ExceptionController 是用于统一处理 Spring MVC 控制器中的异常。让我逐步分析它们的作用,以及为什么在某些情况下它返回错误值。

//1. GlobalExceptionHandler 的作用
@RestControllerAdvice:这个注解用来全局处理控制器层的异常。它会拦截所有抛出的异常,并根据异常类型调用相应的 @ExceptionHandler 方法。

@ExceptionHandler(Exception.class):这个注解标注的方法会在控制器抛出 Exception 或其子类时执行。它用来捕获并处理全局的异常,比如你代码中的 RuntimeException。

globalException(Exception e):这是一个全局异常处理方法。当控制器中出现 Exception 时,这个方法会被调用。它将返回一个带有错误状态码的 Result<Boolean> 对象,并且会将错误信息记录到日志中。

//2. ExceptionController 的作用
@RestController:声明这个类是一个 Spring MVC 控制器,处理 Web 请求并返回数据。

runtimeException() 方法:在这个方法中,你手动抛出了一个 RuntimeException,这会触发 GlobalExceptionHandler 中的 globalException 方法,并返回一个包含错误信息的 Result<Boolean> 对象。

//3. 为什么只有引用 runtimeException() 才返回错误值
runtimeException() 方法直接抛出了一个 RuntimeException,因此会被 GlobalExceptionHandler 捕获并处理。这就是为什么在访问 /exception/runtimeexception 时,你会看到返回的是错误信息。

//4. 在 get() 方法中返回 200 状态码的原因
在 get() 方法中,如果你传入的 id 是无效的,返回的 Result<User> 仍然会是 Result.success(userService.getById(id)),即使 userService.getById(id) 返回的是 null。这种情况下,你的 Result.success(null) 仍然会返回状态码 200,因为 Result.success() 的设计是用于表示成功状态的,且你没有抛出任何异常。

//5. 如何让 get() 方法在出错时返回错误信息
你可以通过以下方法来确保在 get() 方法中传入无效的 id 时,抛出异常并触发全局异常处理器:

手动抛出异常:

java
复制代码
@GetMapping(value = "/{id}")
public Result<User> get(@PathVariable Integer id) {
    User user = userService.getById(id);
    if (user == null) {
        throw new RuntimeException("User not found with id: " + id);
    }
    return Result.success(user);
}
在 userService.getById(id) 方法中抛出异常:如果你的业务逻辑要求在找不到用户时抛出异常,那么可以在 userService.getById(id) 方法中实现这个逻辑。

//6. 总结
GlobalExceptionHandler 用于捕获和处理全局异常。
当你手动抛出 RuntimeException 或其他异常时,它会捕获并返回带有错误信息的 Result。
在 get() 方法中,如果你想要在找不到用户时返回错误信息,需要手动抛出异常,这样才能触发 GlobalExceptionHandler。

日志级别

方法一:直接编写使用
// [配置方法:一种是直接在application.yml文件中配置、另一种是在外置logback-spring.xml文件中配置]
logging:
  pattern:
    console: "%d - %m%n"
方法二:引用外置xml文件
resources/pom.xml               <引用外部的配置>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
</dependency>

resources/application.yaml
logging:
  config: classpath:logback-spring.xml
               <如果你有更多样的配置需求,就需要使用外置XML文件的配置方式>
<?xml version="1.0" encoding="UTF-8" ?>

<configuration>

    <!--    日志文件存放路径-->
    <property name="PATH" value="C:/Users/Pluminary/Desktop/log"/>

    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <!-- 文件日志格式 -->
    <property name="FILE_LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n"/>

    <!-- 控制台输出配置-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>
                ${CONSOLE_LOG_PATTERN}
            </pattern>
        </layout>
    </appender>

    <!-- INFO 级别日志文件输出配置-->
    <appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--按级别过滤日志,只输出 INFO 级别-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!--当天日志文件名-->
        <File>${PATH}/info.log</File>
        <!--按天分割日志文件-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--历史日志文件名规则-->
            <fileNamePattern>${PATH}/info.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
            <!--按大小分割同一天的日志-->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>${FILE_LOG_PATTERN}</Pattern>
        </layout>
    </appender>

    <!-- ERROR 级别日志文件输出配置-->
    <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--按级别过滤日志,只输出 ERROR 及以上级别-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <!--当天日志文件名-->
        <File>${PATH}/error.log</File>
        <!--按天分割日志文件-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--历史日志文件名规则-->
            <fileNamePattern>${PATH}/error.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
            <!--按大小分割同一天的日志-->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>${FILE_LOG_PATTERN}</Pattern>
        </layout>
    </appender>

    <!--日志级别-->
    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="info"/>
        <appender-ref ref="error"/>
    </root>
</configuration>
                   
                   
<
Logback 能够精确区分并输出特定日志级别的错误,是通过 Appender 配置中的 Filter 机制实现的。在你的 Logback 配置文件中,RollingFileAppender 使用了不同的 Filter 来确保只有指定级别的日志信息会被记录到特定的日志文件中。

工作原理
LevelFilter 和 ThresholdFilter:

LevelFilter: 这个过滤器允许你指定只接受特定日志级别的日志。例如,LevelFilter 被配置为只接受 INFO 级别的日志,而拒绝其他级别的日志。<level>INFO</level> 表示只记录 INFO 级别的日志。
ThresholdFilter: 这个过滤器允许你指定一个日志级别的下限,只有高于或等于这个级别的日志才会被记录。例如,ThresholdFilter 被配置为只接受 ERROR 级别及以上的日志(例如 ERROR 和 FATAL)。
日志级别的传递:

日志框架从最底层(比如 TRACE)开始逐级向上检查日志的级别,直到它与 Appender 中配置的 Filter 级别匹配。例如,如果一个 ERROR 级别的日志被触发,RollingFileAppender 的 ThresholdFilter 将检测到这个日志并允许它通过,然后将日志写入指定的 error.log 文件。
日志级别匹配:

当应用程序运行时,它会生成不同级别的日志信息(如 DEBUG、INFO、WARN、ERROR 等)。每个 Appender 都会根据它的 Filter 规则检查这些日志条目。只有符合条件的日志条目才会被记录到相应的日志文件中。
>

AOP切面

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
com/pcy/controller/AspectController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
    @GetMapping
    public Result aspect(String message){
        log.info("aspect controller");
        return Result.success(message);
    }
}
com/pcy/Swagger/SwaggerConfig.java //【增加"/aspect/**"】
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// http://localhost:8080/swagger-ui/index.html
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**","/aspect/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
com/pcy/utils/WebAspect.java
package com.pcy.utils;

import com.pcy.entity.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@Aspect
@Component
public class WebAspect {
// ★★★★★★★★★★★ 一定要注意这个AOP切面扫描的包 ★★★★★★★★★★★
    @Pointcut("execution(public * com.pcy.controller.*.*(..))")
    public void pointCut() {
    }

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("======================================== 这是@Before ========================================");
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getName();
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        Map<String, Object> paramMap = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
        }

        log.info("before path:{}",request.getServletPath());
        log.info("before class name:{}",className);
        log.info("before method name:{}",methodName);
        log.info("before args:{}",paramMap.toString());
    }

    @After(value = "pointCut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("======================================== 这是@After =========================================");
        log.info("{} after", joinPoint.getSignature().getName());
    }

    @AfterReturning(value = "pointCut()", returning = "returnVal")
    public void afterReturning(JoinPoint  joinPoint, Object returnVal) {
        System.out.println("==================================== 这是@AfterReturning ====================================");
        log.info("{} after return, returnVal: {}", joinPoint.getSignature().getName(), returnVal);
    }
}

/*
2024-08-14 18:28:20.249  INFO 3296 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-14 18:28:20.249  INFO 3296 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-08-14 18:28:20.250  INFO 3296 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2024-08-14 18:28:20.261  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:18:28:20:261 毫秒
======================================== 这是@Before ========================================
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before path:/aspect
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before method name:aspect
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before args:{message=www}
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.controller.AspectController      : aspect controller
==================================== 这是@AfterReturning ====================================
2024-08-14 18:28:20.279  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : aspect after return, returnVal: Result(code=200, message=操作成功, data=www)
======================================== 这是@After =========================================
2024-08-14 18:28:20.280  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : aspect after
2024-08-14 18:28:20.308  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:18:28:20:308 毫秒
2024-08-14 18:28:20.308  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-14 18:28:20.309  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:47 毫秒

*/
若是调用UserController的get接口
com/pcy/controller/UserController.java
...
    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @GetMapping("/user/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }
...

Console控制台的报错信息:
/*
2024-08-14 18:33:22.383  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:18:33:22:383 毫秒
======================================== 这是@Before ========================================
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before path:/users/user/2
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.UserController
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before method name:get
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before args:{id=2}
==================================== 这是@AfterReturning ====================================
2024-08-14 18:33:22.425  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : get after return, returnVal: User(id=2, name=we2, age=2, email=2, birthDay=2024-08-10)
======================================== 这是@After =========================================
2024-08-14 18:33:22.426  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : get after
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:18:33:22:428 毫秒
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:45 毫秒
*/
异常善后处理
com/pcy/controller/AspectController.java //【浏览exception接口的时候会报错】
package com.pcy.controller;

import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
    @GetMapping
    public Result aspect(String message){
        log.info("aspect controller");
        return Result.success(message);
    }
    @GetMapping("/exception")
    public Result exception(){//抛出异常
        throw new RuntimeException("runtime exception");
    }
}


/*
======================================== 这是@Before ========================================
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before path:/aspect/exception
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before method name:exception
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before args:{}
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : exception after throwing, message: runtime exception
======================================== 这是@After =========================================
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : exception after
2024-08-15 15:09:20.588 ERROR 4200 --- [nio-8080-exec-4] com.pcy.utils.GlobalExceptionHandler     : runtime exception
*/
com/pcy/utils/WebAspect.java
@AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowing(JoinPoint  joinPoint, Exception e) {
        log.info("{} after throwing, message: {}", joinPoint.getSignature().getName(), e.getMessage());
    }
综上所述:after方法不关心方法是否成功,当方法执行完成之后就会被执行;afterReturning方法必须在目标方法成果return之后才会被执行;afterThrowing方法则会在目标方法抛出异常后被执行
性能统计

Around可以囊括以上所有能力

com/pcy/controller/AspectController.java
@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
  @GetMapping("/sleep/{time}")
    public Result sleep(@PathVariable("time") long time) {
        log.info("sleep");
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
           log.error("error", e);
        }
        if (time == 1000) {
            throw new RuntimeException("runtime exception");
        }
        log.info("wake up");
        return Result.success("wake up");
    }
}
@Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        log.info("around start");
        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.error("around error",e);
        }
        long endTime = System.currentTimeMillis();
        log.info("execute time:{} ms",endTime - startTime);
        return result;
    }
//【当输入time值为2004时】
2024-08-15 15:27:21.987  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:15:27:21:987 毫秒
2024-08-15 15:27:21.990  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : around start
======================================== 这是@Before ========================================
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before path:/aspect/sleep/2004
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before method name:sleep
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before args:{time=2004}
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.controller.AspectController      : sleep
2024-08-15 15:27:23.996  INFO 10844 --- [nio-8080-exec-6] com.pcy.controller.AspectController      : wake up
==================================== 这是@AfterReturning ====================================
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : sleep after return, returnVal: Result(code=200, message=操作成功, data=wake up)
======================================== 这是@After =========================================
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : sleep after
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : execute time:2007 ms
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:15:27:23:999 毫秒
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:12 毫秒


//【当输入time值为1000时】
2024-08-15 15:28:19.596  INFO 10844 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:15:28:19:596 毫秒
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : around start
======================================== 这是@Before ========================================
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before path:/aspect/sleep/1000
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before method name:sleep
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before args:{time=1000}
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.controller.AspectController      : sleep
2024-08-15 15:28:20.607  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : sleep after throwing, message: runtime exception
======================================== 这是@After =========================================
2024-08-15 15:28:20.607  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : sleep after
2024-08-15 15:28:20.607 ERROR 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : around error

java.lang.RuntimeException: runtime exception
    at com.pcy.controller.AspectController.sleep(AspectController.java:32)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
......
同一切面内的执行顺序

先执行before方法,再执行afterReturning / afterThrowing方法,最后执行after方法
要验证的关键点是around方法和它们之间的先后关系

around方法早于before方法开始执行,并且晚于after方法结束执行,刚好将其他同志完全包裹了起来

//【注释掉WebAspect.java里面的代码不然会叠叠乐累加】
com/pcy/utils/AspectOne.java
package com.pcy.utils;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectOne {
    @Pointcut("execution(public * com.pcy.controller.*.*(..))")
    public void pointCut(){}
    @Before(value = "pointCut()")
    public void before(){
        log.info("before one");
    }
    @After(value = "pointCut()")
    public void after(){
        log.info("after one");
    }
    @AfterReturning(value = "pointCut()")
    public void afterReturning(){
        log.info("afterReturning one");
    }

    @Around(value = "pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        log.info("around one start");
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.error("around error", e);
        }
        log.info("around one end");
        return result;
    }
}

/*
2024-08-15 16:12:12.819  INFO 28788 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-15 16:12:12.819  INFO 28788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-08-15 16:12:12.820  INFO 28788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2024-08-15 16:12:12.839  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:16:12:12:839 毫秒
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : around one start
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : before one
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.controller.AspectController      : aspect controller
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : afterReturning one
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : after one
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : around one end
2024-08-15 16:12:12.908  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:16:12:12:908 毫秒
2024-08-15 16:12:12.909  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-15 16:12:12.909  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:69 毫秒
*/
不同切面间的执行顺序

将AspectOne复制两份命名AspectTwo和AspectThree [执行后是One→Three→Two]
在Spring中的加载顺序是根据类名升序排列的,Three字母排序排在Two前面
那如何指定执行顺序按照One Two Three?
分别为AspectOne/Two/Three加上@Order(1),@Order(2),@Order(3)

Redis

集成
pom.xml
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
spring:
  application:
    name: Pluminary
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/pcy?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&nullCatalogMeansCurrent=true
    username: root
    password: root

    redis:
      host: localhost port:6379
      connect-timeout: 1000
      jedis:
        pool:
          min-idle: 5
          max-active: 10
          max-idle: 10
          max-wait: 2000
com/pcy/controller/HelloController.java
@Slf4j
@RestController
@RequestMapping("/test")
public class HelloController {
 @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/hello")
    public String hello(){
        stringRedisTemplate.opsForValue().set("hello","world");
        return stringRedisTemplate.opsForValue().get("hello");
    }
}

//先访问hello接口 再去redis-cli中尝试访问自己定义的内容
http://localhost:8080/swagger-ui/index.html#/hello-controller/hello

/*
127.0.0.1:6379> get hello
"world"
*/

Spring Security

pom.xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
com/pcy/controller/HelloController.java
package com.pcy.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/test")
public class HelloController {
    @GetMapping("/hi")
//  http://localhost:8080/hi
    public String hi(){
        log.info("hi");
        return "ok!";
    }

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/hello")
    public String hello(){
        stringRedisTemplate.opsForValue().set("hello","world");
        return stringRedisTemplate.opsForValue().get("hello");
    }
}

/* Console:
Using generated security password: 4147707e-58d6-46d9-b5cc-19865a2c523f
*/

账号:user
密码:4147707e-58d6-46d9-b5cc-19865a2c523f
com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // 配置HTTP安全性
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
//          .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // 允许访问Swagger UI和API文档
                                .anyRequest().authenticated() // 所有请求都需要认证
                )
                .httpBasic(withDefaults()); // 使用HTTP Basic认证

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .inMemoryAuthentication()
                .withUser("pcy")
                .password(passwordEncoder().encode("123456"))
                .roles("admin");

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

/*
你可能无法访问 http://localhost:8080/swagger-ui/index.html 的原因可能与 Spring Security 配置有关。由于你启用了 Spring Security,默认情况下,所有请求都需要经过身份认证,这可能会阻止你访问 Swagger UI。

为了确保你能够访问 Swagger UI,你需要在 Spring Security 的配置中添加一个例外规则,允许对 /swagger-ui/** 和相关的 Swagger 资源进行无认证访问。

添加代码:.antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()


账号:pcy
密码:123456
*/

从数据库中获取用户信息

com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .anyRequest().authenticated()
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults())
                .csrf(csrf -> csrf.disable());

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .userDetailsService(userDetailsService)  // 使用数据库中的用户信息
                .passwordEncoder(passwordEncoder());

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
com/pcy/service/impl/UserDetailsServiceImpl.java
package com.pcy.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pcy.entity.SysUser;
import com.pcy.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
//确保你的 UserDetailsServiceImpl 类被 Spring 管理,且实现了 Spring Security 的 UserDetailsService 接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService  {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
        if (sysUser == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        return User.builder()
                .username(sysUser.getUsername())
                .password(sysUser.getPassword())
                .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getRole()))
                .build();
    }
}
com/pcy/entity/SysUser.java
package com.pcy.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Schema(name = "SysUser对象", description = "系统用户表")
public class SysUser extends Model<SysUser> {

    private static final long serialVersionUID = 1L;

    @Schema(description = "主键 id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @Schema(description = "用户名")
    private String username;

    @Schema(description = "密码")
    private String password;

    @Schema(description = "角色")
    private String role;


    @Override
    public Serializable pkVal() {
        return this.id;
    }
}
com/pcy/service/SysUserService.java
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.SysUser;

public interface SysUserService extends IService<SysUser> {
    String getCurrentUser();
}
com/pcy/service/impl/SysUserServiceImpl.java
package com.pcy.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pcy.common.ApiException;
import com.pcy.entity.SysUser;
import com.pcy.mapper.SysUserMapper;
import com.pcy.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    @Override
    public String getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 非匿名用户访问才能获得用户信息
        if (!(authentication instanceof AnonymousAuthenticationToken)) {
            String userName = authentication.getName();
            log.info("userName by SecurityContextHolder: {}", userName);
            return userName;
        }
        throw new ApiException("用户不存在!");
    }
}
com/pcy/mapper/SysUserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.SysUser;

/**
 * <p>
 * 系统用户表 Mapper 接口
 * </p>
 */
public interface SysUserMapper extends BaseMapper<SysUser> {

}
com/pcy/common/ApiException.java
package com.pcy.common;

import com.pcy.entity.MessageEnum;
import lombok.Data;

@Data
public class ApiException extends RuntimeException {

    private Integer code;

    public ApiException(MessageEnum messageEnum) {
        super(messageEnum.getMessage());
        this.code = messageEnum.getCode();
    }

    public ApiException(String message) {
        super(message);
        this.code = 500;
    }
}
//【由于数据库的密码要被加密后的形式保存到数据中】
com/pcy/common/test.java
package com.pcy.common;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class test {
    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encodedPassword = encoder.encode("123456");
        System.out.println(encodedPassword);
    }
}

//$2a$10$GzDPdLyrzC9NudmE937AAetR2bef2VQzuSbP6KM6Y.I3045OuT/xC
修改创建SysUser用户的时候用Spring Security [登录的时候就可以用自己创建的了]
com/pcy/controller/UserController.java
/* 对比User数据
    @Operation(summary = "创建User用户", description = "创建一个新的User用户")
    @PostMapping("/create/")
    public User create(@RequestBody User User) {
        return userRepository.save(User);
    }
*/
    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Operation(summary = "创建SysUser用户", description = "创建一个新的SysUser用户")
    @PostMapping("/create/test")
    public SysUser create(@RequestBody SysUser sysUser) {
        sysUser.setPassword(passwordEncoder.encode(sysUser.getPassword()));
        sysUserService.save(sysUser);
        return sysUser;
    }
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.SysUser;
public interface SysUserService extends IService<SysUser>{

    String getCurrentUser();
}
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

 @Override
    public boolean save(SysUser sysUser) {
        return SqlHelper.retBool(this.baseMapper.insert(sysUser));
    }
}
com/pcy/mapper/SysUserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.SysUser;

/**
 * <p>
 * 系统用户表 Mapper 接口
 * </p>
 */
public interface SysUserMapper extends BaseMapper<SysUser> {

}
权限控制
com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers("/security/permitall").permitAll() // 允许所有人访问
                        .requestMatchers("/security/anonymous").anonymous() // 仅允许匿名用户访问
                        .requestMatchers("/security/config").hasAuthority("ROLE_config") // 仅拥有 ROLE_config 权限的用户可以访问
                        .requestMatchers("/security/Secured").hasRole("Secured") // 仅拥有 ROLE_Secured 的用户可以访问
                        .requestMatchers("/security/preAuthorize").hasAuthority("PreAuthorize") // 仅拥有 PreAuthorize 权限的用户可以访问
                        .anyRequest().authenticated() // 其他所有请求需要认证
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults()) // 使用 HTTP Basic 认证
                .csrf(csrf -> csrf.disable()); // 禁用 CSRF

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .userDetailsService(userDetailsService)  // 使用数据库中的用户信息
                .passwordEncoder(passwordEncoder());

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
com/pcy/controller/SecurityController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/security")
@Tag(name = "权限控制", description = "权限控制")
public class SecurityController {
    // Anyone
    @Operation(summary = "permitAll 权限")
    @GetMapping(value = "/permitall")
    public Result<String> permitAll(){
        return Result.success("permitAll");
    }
    // 未登录时可以访问
    @Operation(summary = "anonymous 权限")
    @GetMapping(value = "/anonymous")
    public Result<String> anonymous(){
        return Result.success("anonymous");
    }
    // xiaopan可以访问
    @Operation(summary = "config 权限")
    @GetMapping(value = "/config")
    public Result<String> config(){
        return Result.success("permitAll");
    }
    // xiaochun可以访问
    @Operation(summary = "Secured 权限")
    @GetMapping(value = "/Secured")
    @Secured({"ROLE_Secured"})
    public Result<String> Secured(){
        return Result.success("Secured");
    }
    // panchunyao可以访问
    @Operation(summary = "PreAuthorize 权限")
    @GetMapping(value = "/preAuthorize")
    @PreAuthorize("hasAnyAuthority('PreAuthorize')")
    public Result<String> PreAuthorize(){
        return Result.success("PreAuthorize");
    }
}

/*
首先,确保在数据库中创建几个测试用户,并为每个用户分配不同的角色或权限。假设你有以下几个用户:
User 1: Username: xiaopan, Password: 123456, Role: ROLE_config
User 2: Username: xiaochun, Password: 123456, Role: ROLE_Secured
User 3: Username: panchun, Password: 123456, Authority: PreAuthorize

尝试使用不同用户登录:
使用 xiaopan 登录后,尝试访问 /security/config。
使用 xiaochun 登录后,尝试访问 /security/Secured。
使用 panchunyao 登录后,尝试访问 /security/preAuthorize。

检查响应:
/security/config: 只有 xiaopan 能访问,其他用户会被拒绝访问。
/security/Secured: 只有 xiaochun 能访问,其他用户会被拒绝访问。
/security/preAuthorize: 只有 panchunyao 能访问,其他用户会被拒绝访问。
/security/permitall: 所有用户都可以访问。
/security/anonymous: 只有未登录的用户可以访问,登录的用户会被拒绝。

验证权限控制
每个请求的响应应该反映你在 SecurityConfig 中配置的权限。
如果用户没有适当的角色或权限,应该会返回 403 Forbidden 或其他错误响应。
*/
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**","/aspect/**","/test/**","/security/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }
}
// 【问答环节】
我的数据库创建的是Role字段但是为什么 下面这些有.hasAuthority 有.hasRole 还有其他的 这是怎么匹配到我数据库 按照你的方式设置的数据的

/*
1. hasRole() 和 hasAuthority() 的区别

hasRole(String role):
hasRole 方法通常用于检查用户是否拥有特定的角色。
Spring Security 会在你传递的角色名称前自动加上 "ROLE_" 前缀。因此,当你使用 hasRole("Secured") 时,实际上它会检查用户是否有 "ROLE_Secured" 这个权限。

hasAuthority(String authority):
hasAuthority 方法用于检查用户是否拥有特定的权限(或授权)。
hasAuthority 不会自动添加任何前缀。所以当你使用 hasAuthority("ROLE_config") 时,它会直接匹配 "ROLE_config",而不会添加任何前缀。


2. 匹配数据库中的角色和权限
hasRole("Secured"):
代码中的 hasRole("Secured") 实际上会匹配数据库中的 ROLE_Secured,因为 hasRole 方法会自动加上 "ROLE_" 前缀。

hasAuthority("ROLE_config"):
代码中的 hasAuthority("ROLE_config") 会直接匹配数据库中的 "ROLE_config",没有任何前缀变化。

hasAuthority("PreAuthorize"):
代码中的 hasAuthority("PreAuthorize") 会直接匹配数据库中的 "PreAuthorize",因为没有添加任何前缀。
*/
    
它为什么能查到我数据库的role字段里面的数据 如果我把这个字段换成test这个名字 它又是怎么去匹配到的呢
/*
Spring Security 默认会使用 UserDetails 接口中的 getAuthorities() 方法来获取用户的权限或角色信息。这些权限或角色信息通常是通过你在 UserDetailsService 实现类中定义的逻辑从数据库中获取的。
而在SecurityConfig中有代码:
@Autowired // 使用 Spring Security 的UserDetailsService
private UserDetailsService userDetailsService; 
回顾securityFilterChain代码
下面会有 .userDetailsService(userDetailsService) // 设置 UserDetailsService

Spring Security 本身并不直接访问你的数据库表或字段。它依赖于你在 UserDetailsService 中提供的 UserDetails 对象的 getAuthorities() 方法的返回值。因此,当你在 SecurityConfig 中使用 hasRole() 或 hasAuthority() 方法时,它实际上是在检查用户的权限信息,即 UserDetails 对象中的 authorities。
===========================================================================
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
    if (sysUser == null) {
        throw new UsernameNotFoundException("User not found with username: " + username);
    }
    return User.builder()
            .username(sysUser.getUsername())
            .password(sysUser.getPassword())
            .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getTest()))  // 修改为使用 'test' 字段
            .build();
}

*/

记住我 √ Remember Me

基于SpringSession的方式
pom.xml
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
//【要新搞个登录界面 .ftl】
application.yaml

spring:
  freemarker:
    template-loader-path: /templates/
    suffix: .ftl
resources/templates/loginPage.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
</head>
<body>
<h1>Login</h1>
<form action="/login" method="post">
    <div>
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required>
    </div>
    <div>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required>
    </div>
    <div>
        <input type="checkbox" id="remember-me" name="remember-me">
        <label for="remember-me">Remember me</label>
    </div>
    <div>
        <button type="submit">Login</button>
    </div>
</form>
</body>
</html>
com/pcy/controller/LoginController.java
package com.pcy.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {
    @GetMapping("/login") // 修改为 "/custom-login"
    public String login() {
        return "loginPage"; // 返回的视图名仍然是 "loginPage"
    }
}

/*
http://localhost:8080/login

Please sign in
Username
    panchunyao
Password
    •••••••••••••
√ Remember me on this computer.


127.0.0.1:6379> keys spring*
1) "spring:session:sessions:96c83240-f939-4fd1-ac2c-93542f883aef"
2) "spring:session:sessions:56baf3c6-7a5c-483b-b04a-422b8a2be1b7"
*/
com/pcy/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers("/security/permitall").permitAll() // 允许所有人访问
                        .requestMatchers("/security/anonymous").anonymous() // 仅允许匿名用户访问
                        .requestMatchers("/security/config").hasAuthority("ROLE_config") // 仅拥有 ROLE_config 权限的用户可以访问
                        .requestMatchers("/security/Secured").hasRole("Secured") // 仅拥有 ROLE_Secured 的用户可以访问
                        .requestMatchers("/security/preAuthorize").hasAuthority("PreAuthorize") // 仅拥有 PreAuthorize 权限的用户可以访问
                        .anyRequest().authenticated() // 其他所有请求需要认证
                )
                .formLogin(form -> form
//                        .loginPage("/custom-login") // 将登录页面的路径改为 "/custom-login" 打开会循环重定向
                        .defaultSuccessUrl("http://localhost:8080/swagger-ui/index.html", true) // 登录成功后的跳转路径
                        .permitAll()
                )
                .rememberMe(rememberMe -> rememberMe
                        .rememberMeServices(rememberMeServices()) // 配置 Remember Me 服务
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults()) // 使用 HTTP Basic 认证
                .csrf(csrf -> csrf.disable()); // 禁用 CSRF

        return http.build();
    }
}
......
//【问答环节】
分析一下freemarker这个配置的含义
    freemarker:
      template-loader-path: /templates/
      suffix: .ftl
/*
这些配置项的含义如下:
template-loader-path: /templates/: 这个配置指定了 Freemarker 模板文件的加载路径,也就是 /templates/ 目录。Spring Boot 会在这个目录下寻找所有的 .ftl 模板文件。

suffix: .ftl: 这个配置指定了 Freemarker 模板文件的文件后缀,也就是 .ftl。在控制器中返回视图名称时,Spring 会自动添加这个后缀来查找相应的模板文件。


配置的工作原理
@Controller
public class LoginController {
    @GetMapping("/login")
    public String login() {
        return "loginPage"; // 返回的视图名
    }
}
在 LoginController 中的 login() 方法中,你返回的是 "loginPage":
因为在 application.yaml 中已经配置了 template-loader-path 和 suffix,Spring Boot 会根据这些配置来查找模板文件:
它会在 template-loader-path 配置的 /templates/ 目录下寻找文件。
它会在视图名称 "loginPage" 后面自动添加 .ftl 后缀。
因此,最终 Spring Boot 会查找路径 /templates/loginPage.ftl,并使用这个模板文件来渲染登录页面。这就是为什么当你在控制器中返回 "loginPage" 时,Freemarker 能正确地找到并渲染 loginPage.ftl 模板。

如果你想修改 Freemarker 模板文件的目录或后缀,可以调整 application.yaml 中的相应配置。
视图名称在 return 中不需要包含目录或后缀,Spring 会根据配置自动处理。
*/

SpringBoot + Vue企业级狐狸

@RequestMapping 注解指定控制器类中的方法可以处理哪些格式的URL请求
@RequestMapping("/hello") 说明该方法将接收并处理格式为/hello的HTTP请求
@RestController 注解指定本类承担着SpringBoot项目的'控制器'效果
包名 所放置的业务代码类型
common 放置了通用的参数和业务方法
controller 放置了针对各业务请求的控制类
domain 放置了各种业务实体类
mapper 放置了针对MyBatis框架的映射关系类
service 放置了诸多实现业务逻辑的类
阅读全文

苍穹外卖

2024/9/28

苍穹外卖

软件开发整体介绍

软件开发流程

需求分析
  • 需求规格说明书(word)、产品原型
设计
  • UI设计、数据库设计、接口设计
编码
  • 项目代码、单元测试
测试
  • 测试用例、测试报告
上线运维
  • 软件环境安装、配置
角色分工
  • 项目经理:对整个项目负责,任务分配、把控进度
  • 产品经理:进行需求调研,输出需求调研文档、产品原型等
  • UI设计师:根据产品原型输出界面效果图
  • 架构师:项目整体架构设计、技术选型等
  • 开发工程师:代码实现
  • 测试工程师:编写测试用例,输出测试报告
  • 运维工程师:软件环境搭建、项目上线
软件环境
  • 开发环境:开发人员在开发阶段使用的环境

  • 测试环境:专门给测试人员使用的环境,用于项目测试

  • 生产环境:线上环境

第二轮补充知识点复习 会以橙色标注

苍穹外卖项目介绍

项目介绍
  • 定位:专门为餐饮制定的一款软件产品[管理端用户端]
功能架构 (体现项目中的业务功能模块)
  • 管理端:员工、分类、菜品、套餐、订单管理、工作台、数据统计、来单提醒
  • 用户端:微信登录、商品浏览、购物车、用户下单、微信支付、历史订单、地址管理、用户催单
产品原型“在文件里有用户端和管理端” (用于展示项目的业务功能 一般由产品经理进行设计)
技术选型 (展示项目中使用到的技术框架和中间件)
  • 用户层:node.js、VUE.js、ElementUI、微信小程序、apache echarts

  • 网关层:Nginx

  • 应用层:SpringBoot、SpringMVC、SpringTask、httpclient、SpringCache、JWT、阿里云OSS、Swagger、POI(操作excel表格)、WebSocket(网络协议<催单…>)

  • 数据层:MySQL、Redis、MyBatis、PageHelper、Spring Data Redis

  • 工具:Git、Maven、Junit、PostMan

开发环境搭建

前端:管理端(Web基于Nginx)、用户端(小程序)

前端环境位置:
E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day01\前端运行环境\nginx-1.20.2\html\sky

D:\nginx-1.20.2 [放在英文目录下 双击 nginx.exe] 默认端口号80

[苍穹外卖] (http://localhost/#/login) 如果被其他占用(比如RAGFlow)就把 localhost 换成 127.0.0.1

后端:后端服务(Java)

后端环境位置:
E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day01\后端初始工程\sky-take-out

把sky-take-out导入到idea

  • sky-take-out [maven父工程,统一管理依赖版本聚合其他子模块]
    • sky-common [子模块,存放公共类(工具类、常量类、异常类)]
    • sky-pojo [子模块,存放实体类、VO、DTO等]
    • sky-server [子模块,后端服务,存放配置文件、Controller、Service、Mapper等]
名称 说明
Entity 实体,通常和数据库中的表对应
DTO 数据传输对象,通常用程序中各层之间传递数据
VO 视图对象,为前端展示数据提供的对象
POJO 普通Java对象,只有属性和对应的Getter和Setter

深刻理解POJO

POJO的内在含义是指:那些没有继承任何类、也没有实现任何接口[可以实现],更没有被其它框架侵入的java对象。
POJO是一个简单的、普通Java对象,它包含业务逻辑处理或持久化逻辑等,但不是JavaBean、EntityBean等不具有任何特殊角色,不继承或不实现任何其它Java框架的类或接口。 可以包含类似与JavaBean属性和对属性访问的setter和getter方法的
一般在web应用程序中建立一个数据库的映射对象时,我们只能称它为POJO。

  • POJO持久化之后==〉PO(在运行期,由Hibernate中的cglib动态把POJO转换为PO,PO相对于POJO会增加一些用来管理数据库entity状态的属性和方法。PO对于programmer来说完全透明,由于是运行期生成PO,所以可以支持增量编译,增量调试。)
  • POJO传输过程中==> DTO
  • POJO用作表示层==> VO

深刻理解PO、DTO、VO

PO(persistent object):就是将对象与关系数据库绑定,用对象来表示关系数据,
最简单的PO就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合。PO中应该不包含任何对数据库的操作。

  • 有时也被称为Data对象,对应数据库的entity,简单认为一个PO对应数据库中的一条记录
  • PO中不应该包含任何对数据库的操作
  • PO的属性是跟数据表的字段一一对应的
  • PO对象需要实现序列化接口

DTO(Data Transfer Object): → 数据传输对象
主要用于远程调用需要大量传输对象的地方
我们可以将PO中的部分属性抽取出来,就形成了DTO
举例说明
比如我们有一张表有100个字段,那么对应的PO就有100个属性
但是我们界面上需要显示10个字段,客户端用WEB service来获取数据,没必要把整个PO对象传递到客户端,这时我们就可以用只有这10个属性的DTO来传递结果到客户端,这样就不会暴露服务端表结构,到达客户端后,如果用这个对象来对应界面显示,那么此时它的身份就转为了VO(View Object)


VO
VO(value object) 是值对象,精确点讲它是业务对象,是存活在业务层的,是业务逻辑使用的,它存活的目的就是为数据提供一个生存的地方。VO的属性是根据当前业务的不同而不同的,也就是说,它的每一个属性都一一对应当前业务逻辑所需要的数据的名称。 VO是什么?它是值对象,准确地讲,它是业务对象,是生活在业务层的,是业务逻辑需要了解,需要使用的,再简单地讲,它是概念模型转换得到的。
重点
一个VO可以只是PO的一部分,也可以是多个PO构成,同样也等同于一个PO(指的是属性)。正因为这样,PO独立出来,数据持久层也就独立出来了,它不会受到任何业务的影响和干涉。又因为这样,业务逻辑层也独立开来,它不会受到数据持久层的影响,业务层只关心业务逻辑的处理,怎么存和读都交给别人。

深刻理解什么是DAO

DAO(Data Access Object):数据访问对象
主要用来封装对数据库的访问。通过它可以把POJO持久化为PO,用PO组装出来VO、DTO。
是一个sun的一个标准j2ee设计模式,这个模式中有个接口就是DAO,它负持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和PO结合使用,DAO中包含了各种数据库的操作方法。通过它的方法,结合PO对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合VO,提供数据库的CRUD操作…

  • 主要用来封装对DB(数据库)的访问(CRUD操作)。

  • 通过接收业务层的数据,把POJO持久化为PO。

深刻理解JavaBean

JavaBean是一个遵循特定写法的Java类,是一种Java语言编写的可重用组件,它的方法命名,构造及行为必须符合特定的约定:

1、这个类必须具有一个公共的(public)无参构造函数;
2、所有属性私有化(private);
3、私有化的属性必须通过public类型的方法(getter和setter)暴露给其他程序,并且方法的命名也必须遵循一定的命名规范。
4、这个类应是可序列化的。(比如可以实现Serializable 接口,用于实现bean的持久性)
JavaBean在JavaEE开发中,通常用于封装数据
许多开发者会把JavaBean看作村从特定命名约定的POJOPOJO按照JavaBean的规则来就可以变成JavaBean
当一个POJO可序列化,有一个无参的构造函数,使用getter和setter方法来访问属性时,他就是一个JavaBean
JavaBean是一种组件技术,就好像你做了一个扳手,而这个扳手会在很多地方被拿去用,这个扳子也提供多种功能(你可以拿这个扳手扳、锤、撬等等),而这个扳手就是一个组件。

common里的constant、context、properties代表什么意思

constant:
用于存放常量类。这些常量可能是项目中频繁使用的固定值,如状态码、错误码、系统配置项等。
常量类中的变量一般使用public static final修饰,确保其不可变性。
    
context:
用于存放上下文类。上下文类通常用来保存和传递运行时环境信息或状态。
在Spring框架中,ApplicationContext就是一个典型的上下文对象,它提供了对Bean的访问以及配置信息的管理。
    
properties:
用于存放属性文件。这些文件通常以.properties为扩展名,用于存储配置信息,如数据库连接字符串、系统参数等。
属性文件可以通过Properties类来读取和写入,方便在运行时动态调整系统行为。

Final的巩固

问:对于引用类型(如String、Object等),final变量的引用不能被改变,但引用的对象内部状态可以改变。 这句话是什么意思?

答:当一个引用类型的变量被声明为final时,这个变量的引用(即指向的对象)不能被改变,但该对象的内部状态是可以改变的。我们可以通过具体的例子来理解这一点。
public class Example {
    public static final String EMP_ID = "empId";
    public static void main(String[] args){
        // 下面这行代码会编译失败,因为EMP_ID是final的
        // EMP_ID = "newEmpId"; // 编译错误
        
// 但是可以创建一个新的String对象并使用EMP_ID内容
    String anotherId = EMP_ID + "123";
    sout(anotherId) => empId123;
    }
}

解析context (实现上下类的逻辑原理) 内的代码

package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
//ThreadLocal 是一个线程局部变量,每个线程都有自己的独立副本。这意味着不同线程之间不会共享同一个 ThreadLocal实例的数据,从而避免了多线程环境下的数据竞争问题。
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }
}
/*
这个类通常用于需要在多线程环境中传递和管理线程上下文信息的场景。例如:

Web应用:在处理HTTP请求时,可能需要将用户ID或其他上下文信息绑定到当前线程,以便在整个请求处理过程中都能访问到这些信息。

日志记录:在日志记录中,可能需要记录每个操作的执行者ID,通过 ThreadLocal 可以方便地在日志记录器中获取当前操作者的ID。

事务管理:在分布式事务中,可能需要将事务ID绑定到当前线程,以便在事务的各个阶段都能访问到这个ID。

内存泄漏:如果 ThreadLocal 中存储的对象没有及时释放,可能会导致内存泄漏。因此,建议在不再需要 ThreadLocal 中的数据时,调用 remove 方法将其移除。

线程池:在使用线程池时,特别需要注意 ThreadLocal 的管理。线程池中的线程是复用的,如果不及时清理 ThreadLocal 中的数据,可能会导致数据混淆或内存泄漏。
*/

静态变量解析

//静态变量 (static)
静态变量:在 Java 中,静态变量属于类而不是类的实例。这意味着无论创建多少个类的实例,静态变量都只有一份拷贝,并且所有实例共享这份拷贝。
作用域:静态变量在类加载时初始化,并且在类卸载时销毁。它们存在于类的生命周期内,而不是实例的生命周期内。

//结合 static 和 ThreadLocal
在 BaseContext 类中,threadLocal 被声明为 static,这意味着所有 BaseContext 实例共享同一个 ThreadLocal 实例。但这并不意味着所有线程共享同一个 ThreadLocal 实例的数据。相反,每个线程都有自己独立的 ThreadLocal 数据副本。
    
静态变量:
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
这行代码声明了一个静态的 ThreadLocal 变量 threadLocal,所有 BaseContext 实例共享这个 ThreadLocal 实例。

使用Git进行版本控制

  • 创建Git本地仓库
  • 创建Git远程仓库[GitHub、Gitee]
  • 将本地文件推送到Git远程仓库
.gitignore[文件设置]
    
//忽略git管理的文件
**/target/
.idea
*.iml
*.class
*Test.java
**/test/

创建远程仓库流程:【提交到本地】
VCS → Create Git Repository → sky-take-out → √ → Unversinoed Files(All) → Commit

去创建一个仓库:[Pluminary/sky-take-out (gitee.com)] (https://gitee.com/Pluminary/sky-take-out)

推送代码到Gitee远程仓库:Idea右上角的↗ → 定义一下本地和远程仓库关联 点击Define remote → Name: origin
URL: https://gitee.com/Pluminary/sky-take-out.git (这个是在Gitee上创建仓库后复制的代码)

推送成功:[Pluminary/sky-take-out (gitee.com)] (https://gitee.com/Pluminary/sky-take-out)

后端环境搭建

数据库环境搭建

Unknown collation: ‘utf8mb4_0900_ai_ci‘的解决方法_unknown collation utf8mb4-CSDN博客

前后端联调
浏览器

↓
Controller:
1.接收并封装参数
2.调用service方法查询数据库
3.封装结果并相应

↓
Service:
1.调用mapper查询数据库
2.密码对比
3.返回结果

↓
Mapper:
1.select * from employee where username = ? 

↓
数据库

IDEA中导入多module的Maven项目无法识别module的解决办法_idea modules太多 mvn clean 对某个module不起作用-CSDN博客

Maven → compile(编译聚合模块 )

[INFO] ————————————————————————
[INFO] Reactor Summary for sky-take-out 1.0-SNAPSHOT:
[INFO]
[INFO] sky-take-out ………………………………… SUCCESS [ 0.003 s]
[INFO] sky-common ………………………………….. SUCCESS [ 2.761 s]
[INFO] sky-pojo ……………………………………. SUCCESS [ 2.227 s]
[INFO] sky-server ………………………………….. SUCCESS [ 1.294 s]
[INFO] ————————————————————————
[INFO] BUILD SUCCESS

在数据库中 新建查询  SELECT VERSION(); 引擎是8.0.33的是正规操作mysql此时对应的任务管理器服务里搜索mysql(名称:MySQL80)开启这个  如果开启了服务里的MySQL那SELECT VERSION()查询就是11.0.5-MariaDB

handler:全局异常处理器

右侧Maven的具体用途

  1. clean
    功能:清除项目构建过程中生成的所有文件,通常包括 target 目录下的内容
    命令:mvn clean
    使用场景:
    在每次构建之前,确保没有旧的构建产物干扰新构建。
    清理项目目录,准备进行新的构建。

  2. validate
    功能:验证项目的正确性,确保所有必要的信息都已就绪
    命令:mvn validate
    使用场景:
    在构建过程的早期阶段,检查项目配置是否正确。
    **确保所有依赖项和资源都可用**。

  3. compile
    功能:编译项目的源代码
    命令:mvn compile
    使用场景:
    编译项目源代码,生成 .class 文件。
    通常在开发过程中频繁使用确保代码可以成功编译

  4. test
    功能:运行项目的单元测试。
    命令:mvn test
    使用场景:
    在代码提交前,确保所有单元测试通过。
    持续集成(CI)过程中,自动运行测试以确保代码质量。

  5. package
    功能:将编译后的代码打包成可分发的格式,如 JAR、WAR 等
    命令:mvn package
    使用场景:
    构建项目并生成可部署的包。
    通常在开发和部署过程中使用,生成最终的可发布版本。

  6. verify
    功能:运行任何检查以验证包的完整性和有效性。
    命令:mvn verify
    使用场景:
    在发布前,进行更严格的验证,确保包的质量。
    运行集成测试、性能测试等。

  7. install
    功能:将包安装到本地 Maven 仓库,供其他项目使用
    命令:mvn install
    使用场景:
    将项目依赖安装到本地仓库,以便其他项目可以引用。
    通常在开发和测试环境中使用,确保依赖项可用。

  8. site
    功能:生成项目的站点文档,包括项目报告、测试覆盖率等。
    命令:mvn site
    使用场景:
    生成项目文档,供团队成员和外部用户查阅。
    文档生成和发布,提高项目的透明度和可维护性。

  9. deploy
    功能:将最终的包部署到远程仓库,如 Nexus、Artifactory 等。
    命令:mvn deploy
    使用场景:
    将项目发布到远程仓库,供其他团队或项目使用。
    通常在持续集成和持续部署(CI/CD)流程中使用,确保发布的版本可用。

总结
clean:清理构建产物。
validate:验证项目配置。
compile:编译源代码。
test:运行单元测试。
package:打包项目。
verify:验证包的完整性和有效性。
install:安装到本地仓库。
site:生成项目文档。
deploy:部署到远程仓库。

思考:前端发送的请求,是如何请求到后端服务的?
前端请求地址:http://localhost/api/employee/login
后端接口地址:http://localhost:8080/admin/employee/login
nginx 反向代理的配置方式
nginx.conf

server{
    listen 80;
    server_name localhost;
    location /api/{
        proxy_pass http://localhost:8080/admin/;  #反向代理
    }
}
nginx 负载均衡的配置方法(平均转发到多台后端服务器)
nginx.conf

upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

server{
    listen 80;
    server_name localhost;
    location /api/{
        proxy_pass http://webservers/admin/;  #反向代理
    }
}
nginx 负载均衡策略:
名称 说明
轮询 默认方式
weight 权重方式,默认为1,权重越高,被分配的客户端请求就越多
ip_hash 依据ip分配方式,这样每个访客可以固定访问一个后端服务
least_conn 依据最少连接方式,把请求优先分配给连接数少的后端服务
url_hash 依据url分配方式,这样相同的url会被分配到同一个后端服务
fair 依据相应时间方式,响应时间短的服务将会被优先分配

完善登录功能

问题:员工表中的密码是明文存储,安全性太低
  • 将密码加密后存储,提高安全性

  • 使用MD5加密方式对明文密码加密 [不可逆]

  • 修改数据库中的明文代码,改为MD5加密后的密文

  • 修改Java代码,前端提交的代码进行MD5加密后再跟数据库中密码比对

在Idea中有 “//TODO” 这代表着标记处 此处还未完成一些操作 标记后可以在idea的下面快速定位到TODO

MD5密码加密后 也区分大小写 如果相同的密文但是大小写不同 结果还是不同的

修改密码
com/sky/controller/admin/EmployeeController.java
 @PutMapping("/editPassword")
    @ApiOperation("修改密码")
    public Result editPassword(@RequestBody PasswordEditDTO passwordEditDTO) {
        log.info("修改密码:{}", passwordEditDTO);
        employeeService.updatePassword(passwordEditDTO);
        return Result.success();
    }
com/sky/service/EmployeeService.java
/**
     * 更改密码
     * @param passwordEditDTO
     */
    void updatePassword(PasswordEditDTO passwordEditDTO);
com/sky/service/impl/EmployeeServiceImpl.java
 /**
     * 更改密码
     * @param passwordEditDTO
     */
    @Override
    public void updatePassword(PasswordEditDTO passwordEditDTO) {
    //getCurrentId 方法:public static Long getCurrentId() 方法用于获取当前线程的用户ID。

        Long empId = BaseContext.getCurrentId();
    //select * from employee where id = #{id}   根据id查员工的所有
        Employee employee = employeeMapper.getById(empId);
    //用md根据从前端传来的oldpassword 去判断employee的原始代码是否相同
        if (!employee.getPassword().equals(DigestUtils.md5DigestAsHex(passwordEditDTO.getOldPassword().getBytes()))) { 
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }
        String newPassword = DigestUtils.md5DigestAsHex(passwordEditDTO.getNewPassword().getBytes());
        employee.setPassword(newPassword);
        employeeMapper.update(employee);
    }

导入接口文档

前后端分离开发流程
  • 定制接口(定义规范) → 前端开发(mock数据) + 后端开发(后端自测) → 连调(校验格式) → 提测(自动化测试)
操作步骤 YApi Pro-高效、易用、功能强大的可视化接口管理平台

将课程资料中提供的项目接口导入YApi
苍穹外卖-管理端接口.json
苍穹外卖-用户端接口.json
苍穹外卖-管理端+用户端接口 → 数据管理 → 数据导入(json 随后把json文件拖入) → 点击接口可查看

Swagger介绍和使用方式

Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案

<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>knife4j-spring-boot-starter</artifactId>
   <version>3.0.2</version>
</dependency>
使用方式
  • 导入knife4j的maven坐标
  • 在配置类中加入knife4j相关配置
sky-server  com/sky/config/WebMvcConfiguration.java
/**
 * 通过knife4j生成接口文档
 * @return
*/ 
@Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                    //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
  • 设置静态资源映射,否则接口文档页面无法访问
/**
 * 设置静态资源映射
 * @param registry
*/
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

[苍穹外卖项目接口文档] (http://localhost:8080/doc.html#/home) 这个文档是解析EmployeeController来的

通过Swagger就可以生成接口文档,那么我们不需要Yapi了?
  • Yapi是设计阶段使用的工具,管理和维护接口
  • Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

编写接口文档 在企业中需要注意:

测试:
为每个API编写单元测试和集成测试,确保API的正确性和稳定性。
使用自动化测试工具(如Postman, JUnit等)来定期验证API的行为。

Swagger常用注解

注解 说明
@Api 用在类上,例如Controller,表明对类的说明
@ApiModel 用在类上,例如entity、DTO、VO
@ApiModelProperty 用在属性上,描述属性信息
@ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用
sky-server  com/sky/controller/admin/EmployeeController.java
package com.sky.controller.admin;

import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.entity.Employee;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.EmployeeService;
import com.sky.utils.JwtUtil;
import com.sky.vo.EmployeeLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 员工管理
 */
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "员工相关接口")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;
    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 登录
     *
     * @param employeeLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation(value = "员工登录")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
        log.info("员工登录:{}", employeeLoginDTO);

        Employee employee = employeeService.login(employeeLoginDTO);

        //登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);

        EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                .id(employee.getId())
                .userName(employee.getUsername())
                .name(employee.getName())
                .token(token)
                .build();

        return Result.success(employeeLoginVO);
    }

    /**
     * 退出
     *
     * @return
     */
    @PostMapping("/logout")
    @ApiOperation(value = "员工退出")
    public Result<String> logout() {
        return Result.success();
    }
}
sky-pojo  com/sky/vo/EmployeeLoginVO.java
// 这里是最后返回的数据vo [已经经历过由po→DTO→vo的过程] 这里的po应该就是Employee
package com.sky.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {

    @ApiModelProperty("主键值")
    private Long id;

    @ApiModelProperty("用户名")
    private String userName;

    @ApiModelProperty("姓名")
    private String name;

    @ApiModelProperty("jwt令牌")
    private String token;
}
sky-pojo  com/sky/dto/EmployeeLoginDTO.java
// 这里的DTO是传输中的数据
package com.sky.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

@Data
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable {

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("密码")
    private String password;

}

Getter与Setter无中生有??    以及快速创建对象builder

在上述VO和DTO代码中很显然没有看到常见的Getter和Setter
这是因为代码使用了 Lombok 注解,Lombok 是一个 Java 库,可以通过注解自动生成常见的样板代码,如 getter、setter、toString、equals 和 hashCode 等方法。

Lombok 注解解释
@Data
作用:这是一个组合注解,包含了 @ToString、@EqualsAndHashCode、@Getter、@Setter 和 @RequiredArgsConstructor。
效果:自动生成所有字段的 getter 和 setter 方法,toString 方法,equals 和 hashCode 方法,以及一个包含所有 final 字段和 @NonNull 字段的构造函数。

@Builder
作用:生成一个构建器模式的类,使得对象的创建更加灵活和可读。
效果:自动生成一个静态内部类 EmployeeLoginVO.EmployeeLoginVOBuilder,并提供构建方法。

  • 生成的构建器类包含所有字段的设置方法,并提供一个 build 方法来最终构建对象

  • 使用构建器模式可以让你在创建对象时更清晰地指定各个字段的值,特别是在对象有很多字段时。
    构建器模式允许你按需设置字段,而不需要为每个字段组合创建多个构造函数。

  • 生成的构建器类:
    Lombok 会自动生成一个静态内部类 EmployeeLoginVOBuilder,包含所有字段的设置方法和一个 build 方法。

    • 设置字段:
      你可以按需调用构建器的设置方法来设置字段值,例如 id(1L)、userName(“john_doe”) 等。
    • 构建对象:
      最后调用 build 方法来创建 EmployeeLoginVO 对象。
 // 使用构建器创建 EmployeeLoginVO 对象
        EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                .id(1L)
                .userName("john_doe")
                .name("John Doe")
                .token("eyJhbGciOiJIUzI1NiJ9...")
                .build();

        System.out.println(employeeLoginVO);

@NoArgsConstructor
作用:生成一个无参构造函数。
效果:自动生成一个不带任何参数的构造函数。

@AllArgsConstructor
作用:生成一个全参构造函数。
效果:自动生成一个包含所有字段的构造函数。

详细解析@GetMapping 与 @PostMapping

选择使用 @GetMapping 还是 @PostMapping 主要取决于Http请求的性质和用途
@GetMapping

作用:

  • @GetMapping专门用于处理HTTP GET请求

  • GET请求通常用于从服务器获取资源,不会对服务器上的数据进行修改

特点:

  • 请求参数通常附加在URL中 @GetMapping("/xxx/{id}") 底下会跟 @PathVariable
  • 请求是安全的不会修改服务器状态

适用场景:

  • 查询数据:获取用户列表、搜索结果
  • 获取静态资源:图片、css文件
  • 获取单个资源:获取某个用户的详细信息

@PostMapping

作用:

  • @PostMapping专门用于处理HTTP POST请求
  • POST请求通常用于向服务器发送数据,可能会对服务器上的数据进行修改

特点:

  • 请求参数放在请求体中,不会显示在URL这种 @PostMapping("/users")底下会跟@RequestBody
  • 请求体的大小没有固定限制,可以发送大量数据
  • 多次相同的POST请求可能会产生不同的结果,可能会修改服务器状态
  • 请求是不安全的,可能会修改服务器状态

适用场景:

  • 创建新资源:注册新用户、添加新订单
  • 更新现有资源:修改用户信息、更新订单状态
  • 发送表单数据:登录、提交评论
  • 上传文件:图片、文档

以下是一些可能的原因,解释了为什么您看到的前端项目代码不是“正经的网页代码”:

  • 构建过程:现代前端项目通常有一个构建过程,使用工具如Webpack、Rollup或Parcel等将源代码转换和打包。原始的代码可能在src目录中,而您看到的是构建后的dist目录,其中包含了转换和压缩后的文件。
  • 模块化和组件化:前端框架和库鼓励开发者将UI拆分成可复用的模块和组件。这意味着不再有传统的HTML、CSS和JavaScript混合在一起的代码,而是分别在不同的文件中定义。
  • 框架和库的使用:现代前端开发往往依赖于各种框架和库,这些框架和库有自己的文件结构和编写方式,可能和您熟悉的传统网页代码写法有很大不同。

根据@PutMapping(“/{Id}”)提问

我为什么@PutMapping(“/{id}”)是id呢 是不是因为我后面需要数据库where id = #{id} 查询才这样传输 如果 我想以name去查询呢

在RESTful API设计中,资源通常通过其唯一标识符(如ID)来访问和操作。@PutMapping("/{id}")使用{id}作为路径变量是因为ID是数据库中记录的唯一标识,这使得通过ID查找和更新特定的记录变得直接和高效。

如果你想要通过name来查询和更新记录,这在理论上是可行的,但需要注意以下几点:

  1. name字段可能不是唯一的,这意味着可能有多条记录具有相同的名称。
  2. 使用非唯一字段作为更新依据可能会导致数据不一致或错误更新。

更新员工 前后端联调原理

★ 在前端使用Ajax与后端URL对应时,你需要确保Ajax请求的URL与后端控制器中定义的映射相匹配。在你提供的例子中,后端使用@PutMapping("/{id}")来定义更新员工信息的端点,因此前端的Ajax URL应该包含员工的ID

// 绑定更新按钮的点击事件
$('#updateBtn').click(function() {
    var employee = {
        id: $('#id').val(), // 假设这是员工的唯一标识符
        name: $('#name').val(),
        email: $('#email').val(),
        department: $('#department').val()
    };

    // 发起Ajax PUT请求,URL中包含员工的ID
    $.ajax({
        url: `/api/employees/${employee.id}`, // 注意这里的URL与后端的@PutMapping("/{id}")对应
        type: 'PUT',
        contentType: 'application/json', // 指定发送给服务器的数据类型
        data: JSON.stringify(employee), // 将JavaScript对象转换为JSON字符串
        success: function(response) {
            // 请求成功,可以在这里处理响应数据
            alert('Employee information updated successfully!');
            // 如果需要,可以在这里更新页面上的表单数据
        },
        error: function(xhr, status, error) {
            // 请求失败,可以在这里处理错误信息
            alert('Error updating employee information: ' + xhr.responseText);
        }
    });
});

//在这个例子中,employee.id是从表单中获取的员工ID,它被拼接到URL字符串中,以形成完整的请求URL。这个URL应该与后端控制器中定义的@PutMapping("/{id}")相对应。当点击更新按钮时,Ajax请求会被发送到后端,后端会根据提供的ID找到对应的员工记录并进行更新。
 @PutMapping("/{id}")
    public ResponseEntity<Employee> updateEmployee(@PathVariable Long id, @RequestBody Employee employeeDetails) {
        Employee employee = employeeService.getEmployeeById(id);
        if (employee != null) {
            employee.setName(employeeDetails.getName());
            employee.setEmail(employeeDetails.getEmail());
            employee.setDepartment(employeeDetails.getDepartment());
            Employee updatedEmployee = employeeService.updateEmployee(employee);
            return ResponseEntity.ok(updatedEmployee);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}
//这里{id}是路径变量,它会匹配Ajax请求URL中的employee.id。这样,前后端的URL就正确对应起来了。

新增员工(Post+Json提交格式)

需求分析和设计

账号必须是唯一的、手机号校验合法11位、性别单选男女、身份证合法18位号码、新增密码默认为123456

本项目约定

  • 管理端发出的请求,统一使用 /admin 作为前缀
  • 用户端发出的请求,统一使用 /user 作为前缀

代码开发

根据新增员工接口设计对应的DTO
注意:当前提交的数据和实体类中对应的属性差别比较大时,建议使用DTO(数据传输)来封装数据

sky-pojo  com/sky/dto/EmployeeDTO.java
package com.sky.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public class EmployeeDTO implements Serializable {

    private Long id;

    private String username;

    private String name;

    private String phone;

    private String sex;

    private String idNumber;

}
sky-server  com/sky/controller/admin/EmployeeController.java
/**
     * 新增员工
     * @param employeeDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增员工")
    public Result save(@RequestBody  EmployeeDTO employeeDTO){
    // 因为是JSON格式 要加@RequestBody
        log.info("新增员工:{}",employeeDTO);
        employeeService.save(employeeDTO);
        return Result.success();
    }
sky-server  com/sky/service/EmployeeService.java
package com.sky.service;

import com.sky.dto.EmployeeDTO;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.entity.Employee;

public interface EmployeeService {

    /**
     * 员工登录
     * @param employeeLoginDTO
     * @return
     */
    Employee login(EmployeeLoginDTO employeeLoginDTO);

    /**
     * 新增员工
     * @param employeeDTO
     */
    void save(EmployeeDTO employeeDTO);
}
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
 /**
     * 新增员工
     * @param employeeDTO
     */
    @Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        //employee.setName(employeeDTO.getName()); 太多了 用对象属性拷贝
        BeanUtils.copyProperties(employeeDTO,employee); //其余的要手动设置
        //设置账号状态,默认正常状态 1正常 0锁定  规范封装
        employee.setStatus(StatusConstant.ENABLE);
        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        //设置当前记录创建人id和修改人id
        //TODO 后期需要改为当前登录用户的id
        employee.setCreateUser(10L);
        employee.setUpdateUser(10L);

        employeeMapper.insert(employee);
    }
sky-pojo  com/sky/entity/Employee.java
package com.sky.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;

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

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;

    private Integer status;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;

    private Long createUser;

    private Long updateUser;

}
sky-server  com/sky/mapper/EmployeeMapper.java
/**
 * 插入员工数据
*/
    @Insert("insert into employee (name,username,password,phone,sex,id_number,create_time,update_time,create_user,update_user))" +
            "values" +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    void insert(Employee employee);

@Builder 和 @JsonFormat

@Builder 是 Lombok 提供的一个注解,用于自动生成构建器模式的代码。
它会在编译时生成一个静态的 Builder 类和相关的方法,使得对象的创建更加灵活和可读。
需要创建不可变对象时,可以使用 @Builder 结合 @Value 注解。
需要创建复杂的对象时,可以通过构建器模式逐步设置属性,提高代码的可读性和可维护性

@JsonFormat 是 Jackson 库提供的注解,用于指定日期时间字段在 JSON 序列化和反序列化时的格式。
通过设置 pattern 属性,可以控制日期时间字段的格式化方式。
当需要将 LocalDateTime、Date 等日期时间类型的字段转换为特定格式的字符串时。
在 RESTful API 中,返回的 JSON 数据需要符合特定的日期时间格式要求。

使用 @Builder 的场景
public class Main {
    public static void main(String[] args) {
        // 使用 @Builder 创建 Employee 对象
        Employee employee = Employee.builder()
                .id(1L)
                .username("user123")
                .name("张三")
                .password("password123")
                .phone("12345678901")
                .sex("男")
                .idNumber("123456789012345678")
                .status(1)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .createUser(1L)
                .updateUser(1L)
                .build();

        System.out.println(employee);
    }
}
使用 @JsonFormat 的场景
//创建 RESTful API
Employee里面的pojo就不详细写了
    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
==========================================================================

@RestController
public class EmployeeController {

    @GetMapping("/employee")
    public ResponseEntity<String> getEmployee() throws Exception {
        // 创建 Employee 对象
        Employee employee = Employee.builder()
                .id(1L)
                .username("user123")
                .name("张三")
                .password("password123")
                .phone("12345678901")
                .sex("男")
                .idNumber("123456789012345678")
                .status(1)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .createUser(1L)
                .updateUser(1L)
                .build();

        // 使用 ObjectMapper 将 Employee 对象转换为 JSON 字符串
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(employee);

        return ResponseEntity.ok(json);
    }
}
===========================================================================
// 除了 @JsonFormat 注解,还有其他方式可以指定日期时间格式,具体取决于你的需求和使用的库。
 public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedDateTime = now.format(formatter);

        Employee employee = Employee.builder()
                .id(1L)
                .username("user123")
                .name("张三")
                .password("password123")
                .phone("12345678901")
                .sex("男")
                .idNumber("123456789012345678")
                .status(1)
                .createTime(now)
                .updateTime(now)
                .createUser(1L)
                .updateUser(1L)
                .build();

        System.out.println("Formatted Create Time: " + formattedDateTime);
        System.out.println("Formatted Update Time: " + formattedDateTime);
    }

@Builder
通过 Employee.builder() 创建了一个构建器对象。
使用链式调用设置各个属性,最后调用 build() 方法生成 Employee 实例。
这种方式使得创建对象的代码更加简洁和易读,特别是当对象属性较多时。

@JsonFormat
在 createTime 和 updateTime 字段上使用了 @JsonFormat 注解,指定了日期时间的格式为 “yyyy-MM-dd HH:mm:ss”。
当 Employee 对象被转换为 JSON 字符串时,这两个字段会被格式化为指定的日期时间格式。
这样可以确保返回的 JSON 数据符合预期的格式要求。

RESTful风

可缓存性:
RESTful API 可以利用 HTTP 缓存机制,减少网络请求,提高性能。
客户端可以缓存响应,减少服务器的负载。

易于集成:
RESTful API 使用标准的 HTTP 协议,几乎所有的编程语言和框架都支持 HTTP 请求。
这使得不同系统之间的集成变得更加容易。

可读性强:
RESTful API 的 URL 设计通常非常直观,易于理解和记忆。
例如,/users/123 表示用户 ID 为 123 的资源,/users/123/orders 表示该用户的订单资源。

灵活性:
RESTful API 支持多种数据格式(如 JSON、XML 等),可以根据需要选择合适的格式。
客户端和服务器可以通过协商确定数据格式,提高了灵活性。

GET /users
GET /users/{id}
POST /users
PUT /users/{id}

功能测试

功能测试方式:
  • 通过接口文档测试
  • 通过前后端联调测试

注意:由于开发阶段前后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主

[苍穹外卖项目接口文档] (http://localhost:8080/doc.html#/documentManager/GlobalParameters-default)

首先要拿到JWT令牌(去接口进行一次登录测试后会有) → 全局参数设置 → 添加参数
注意:这个jwt→json是有有效期的(2小时=7200000秒)

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token

{
“code”: 1,
“msg”: null,
“data”: {
“id”: 1,
“userName”: “admin”,
“name”: “管理员”,
“token”: “eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzI3NjAxMTAxfQ.rnxaRc7fjPzMYwGHk3VzKA4EOxRFrYkKzesxEQsCQUc”
}
}


新增参数:
参数名称:token
参数值:eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzI3NjAxMTAxfQ.rnxaRc7fjPzMYwGHk3VzKA4EOxRFrYkKzesxEQsCQUc
参数类型:header

新增员工接口
{
“idNumber”: “1321321312”,
“name”: “张三”,
“phone”: “11111111111”,
“sex”: “1”,
“username”: “zhangsan”
}

响应内容:
{
“code”: 1,
“msg”: null,
“data”: null
}

sky-server  com/sky/interceptor/JwtTokenAdminInterceptor.java
package com.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * jwt令牌校验的拦截器
 */
@Component  //将该类注册为 Spring 管理的 Bean。
@Slf4j //使用 Lombok 自动生成日志记录器
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
// 包含 JWT 相关的配置属性,如令牌名称和密钥
    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
//检查当前拦截到的是否是 Controller 的方法。如果不是,直接放行
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
com/sky/properties/JwtProperties.java
package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;

    /**
     * 用户端微信用户生成jwt令牌相关配置
     */
    private String userSecretKey;
    private long userTtl;
    private String userTokenName;
}

代码完善

程序存在的问题:
  • 录入的用户名已存在,抛出异常后没有处理
  • 新增员工时,创建人id和修改人id设置了固定值

当你在 Maven 中执行 compile 命令时,它会强制 Maven 重新编译整个项目,包括所有的类和资源。这一过程会清除任何旧的编译结果,确保所有的依赖和代码都是最新的。这可能导致以下几种情况,从而解决了你的问题:

**重新编译:**Maven 会重新编译所有的源代码,包括你修改或新增的类,这样就能解决因为旧的编译缓存而引起的引用问题。
**更新依赖:**如果你在项目中添加或修改了依赖,执行 compile 可以确保这些依赖被正确加载和引用。
清理旧缓存在编译过程中,Maven 会清理旧的缓存和临时文件,避免由于这些文件造成的潜在冲突。
**IDE 同步:**有时候,IDE 的状态可能与 Maven 项目状态不一致,执行 Maven 命令可以帮助 IDE 重新同步项目的状态。

问题①
sky-server  com/sky/handler/GlobalExceptionHandler.java
package com.sky.handler;

import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;

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

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

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        String message = ex.getMessage();
        if (message.contains("Duplicate entry")) {
            // Duplicate entry 'zhangsan' for key 'employee.idx_username'
// 在这里,我们使用 split("'") 将字符串分割为多个部分。这样,parts[1] 将得到 zhangsan,因为它位于单引号之间。这种方式可以正确提取用户名。
            String[] split = message.split("'");
            String username = split[1];
//            String msg = username + "已存在";
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else {
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
}

Split的深入学习

  • 正则表达式

split 方法接受一个正则表达式作为参数,因此分隔符可以是复杂的模式,而不仅仅是单个字符。
例如,split(“\s+”) 可以用来按一个或多个空白字符(包括空格、制表符、换行符等)进行分割。

  • 限制分割次数

split 方法还有一个重载版本 split(String regex, int limit),可以限制分割的次数。
例如,split(“‘“, 3) 只会进行两次分割,结果数组最多包含三个元素。

问题② 解析出员工登录id后,如何转递给Service的save方法?ThreadLocal

前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:
        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
ThreadLocal

ThreadLocal并不是一个Thread,Thread的局部变量
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获得到对应的值,线程外则不能访问

sky-common  com/sky/context/BaseContext.java
package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }
}
sky-server  com/sky/interceptor/JwtTokenAdminInterceptor.java
package com.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
// ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
/**
     * 新增员工
     * @param employeeDTO
     */
    @Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
//        employee.setName(employeeDTO.getName()); 太多了 用对象属性拷贝
        BeanUtils.copyProperties(employeeDTO,employee); //其余的要手动设置
        //设置账号状态,默认正常状态 1正常 0锁定  规范封装
        employee.setStatus(StatusConstant.ENABLE);
        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        //设置当前记录创建人id和修改人id
        //TODO 后期需要改为当前登录用户的id
//        employee.setCreateUser(10L);
// ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
        employee.setCreateUser(BaseContext.getCurrentId());
        employee.setUpdateUser(BaseContext.getCurrentId());
    
        employeeMapper.insert(employee);
    }

// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 如果想单独针对22行代码 测试部分的值是多少 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 左键选中'BaseContext.getCurrentId()' 右键Evaluate Expression单独计算即可

将员工登录ID放在 ThreadLocal 中的原因

在多线程环境中,使用 ThreadLocal 来存储和传递员工的完整信息是一个常见的做法。这样可以确保每个线程都有独立的变量副本,避免并发问题。以下是一些步骤和最佳实践,帮助你在 ThreadLocal 中安全地传递和存储员工的完整信息。
ThreadLocal 主要用于在同一个线程内传递和存储数据,确保每个线程都有独立的变量副本。在你的例子中,ThreadLocal 用于存储员工ID,确保在多线程环境下员工ID的安全传递。

  • 线程隔离:ThreadLocal 确保每个线程都有独立的员工ID副本,避免了多线程环境下的并发问题。
    防止篡改:只有当前线程可以访问和修改 ThreadLocal 中的员工ID,其他线程无法访问,确保了ID的安全性。
  • 员工其他信息的安全性
    数据库查询:员工的其他信息是从数据库中查询的,而不是从 ThreadLocal 中获取的。数据库查询本身是安全的,只要数据库连接和查询操作是安全的。
    权限控制:确保只有经过认证的用户才能执行查询操作,防止未授权访问。
    数据加密:敏感信息(如密码)在存储和传输过程中应进行加密,确保数据的安全性。

线程安全
ThreadLocal 为每个线程提供独立的变量副本,避免了多线程环境下的并发问题。每个线程都可以安全地读取和修改自己的 ThreadLocal 变量,而不会影响其他线程。

简化代码
在 Web 应用中,通常需要在多个方法或组件之间传递用户身份信息(如员工登录ID)。使用 ThreadLocal 可以避免在每个方法调用中显式传递这些信息,从而简化代码。

全局访问
在同一个线程内,任何地方都可以访问 ThreadLocal 中存储的值,这使得在复杂的业务逻辑中传递和使用员工登录ID变得非常方便。

避免传递参数
在多层调用中,如果需要传递员工登录ID,通常需要在每个方法签名中添加相应的参数。使用 ThreadLocal 可以避免这种繁琐的参数传递,提高代码的可读性和可维护性。

// 设置员工登录ID:
在用户登录成功后,将员工登录ID设置到 ThreadLocal 中。
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
    // 验证用户名和密码
    User user = userService.validateUser(loginRequest.getUsername(), loginRequest.getPassword());
    if (user != null) {
        // 设置当前线程的员工登录ID
        BaseContext.setCurrentId(user.getId());
        // 返回登录成功信息
        return ResponseEntity.ok("Login successful");
    } else {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password");
    }
}
// 获取员工登录ID
在需要使用员工登录ID的地方,直接从 ThreadLocal 中获取
@Service
public class OrderService {

    public void createOrder(Order order) {
        Long currentUserId = BaseContext.getCurrentId();
        if (currentUserId != null) {
            order.setCreatedBy(currentUserId);
            orderRepository.save(order);
        } else {
            throw new RuntimeException("User ID not found in context");
        }
    }
}

将员工登录ID放在 ThreadLocal 中,可以确保每个线程都有独立的变量副本,避免多线程环境下的并发问题。同时,这种方式简化了代码,提供了全局访问的能力,避免了繁琐的参数传递,使得在复杂的业务逻辑中传递和使用员工登录ID变得非常方便。

员工分页查询

需求分析和设计
业务规则:(查询 → get)
  • 根据页码展示员工信息
  • 每页展示10条数据
  • 分页查询时可以根据需要,输入员工姓名进行查询
代码开发
根据分页查询接口设计对应的DTO:

Query

参数名称 是否必须 示例 备注
name 张三 员工姓名
page 1 页码
pageSize 10 每页记录数
@Data
public class EmployeePageQueryDTO implements Serializable{
    private String name;
    private int page;
    private int pageSize;
}
后面所有的分页查询,统一都封装成PageResult对象
/*封装分页查询结果*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable{
    private long total; 
    private List records;
}
员工信息分页查询后端返回的对象类型为:Result < PageResult >
sky-server  com/sky/controller/admin/EmployeeController.java
/**
     * 员工分页查询
     * @param employeePageQueryDTO
     * @return
     */
    @GetMapping("/page")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
        //格式不是JSON不用加 @RequestBody
        log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
        return Result.success(pageResult);
    }
sky-server  com/sky/service/EmployeeService.java
/**
     * 分页查询
     * @param employeePageQueryDTO
     * @return
     */
    PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
/**
     * 分页查询
     * @param employeePageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        // select * from employee limit 0,10
        // 开始分页查询 动态拼接
        PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
        Page<Employee> page =  employeeMapper.pageQuery(employeePageQueryDTO);

        long total = page.getTotal();
        List<Employee> records = page.getResult();

        return new PageResult(total, records);
}

逐行研究分页查询

  • PageHelper.startPage 是 MyBatis 分页插件提供的方法,用于开启分页功能

    • employeePageQueryDTO.getPage() 获取当前页码。

    • employeePageQueryDTO.getPageSize() 获取每页显示的记录数

      这一行代码的作用是告诉 MyBatis 在接下来的查询中启用分页,并设置分页参数

  • employeeMapper.pageQuery(employeePageQueryDTO) 是调用 MyBatis 的 Mapper 接口方法,执行分页查询。

    • employeePageQueryDTO 包含了查询条件,如关键字、排序字段等。
    • 查询结果会被封装成 Page 对象,其中包含了分页数据和分页元数据。
  • page.getTotal() 获取分页查询的总记录数。

    • 总记录数用于计算总页数和其他分页相关的计算
  • List records = page.getResult();

    • page.getResult() 获取分页查询的实际数据列表。
    • 这个列表包含了当前页的员工记录
  • return new PageResult(total, records);

  • new PageResult(total, records) 创建一个新的 PageResult 对象,将总记录数和分页数据列表封装起来

  • PageResult 类通常包含 total 和 records 属性,用于返回给客户端

  • 假设 employeePageQueryDTO.getPage() 返回 2,employeePageQueryDTO.getPageSize() 返回 10,那么 MyBatis 生成的 SQL 可能类似于:

SELECT * FROM employee
WHERE ... -- 根据 employeePageQueryDTO 中的查询条件
LIMIT 10 OFFSET 10;

LIMIT 10:表示每页显示 10 条记录。
OFFSET 10:表示从第 11 条记录开始(因为页码从 1 开始,所以第 2 页的偏移量是 10)。

  • PageResult 类:用于封装分页查询的结果,包括总记录数和当前页的数据集合。
    使用场景:在分页查询服务中,将查询结果封装为 PageResult 对象,通过控制器返回给客户端。

  • Serializable 接口:是 Java 中的一个标记接口,没有定义任何方法。实现 Serializable 接口的类的对象可以被序列化,即将对象的状态转换为字节流,以便在网络上传输或持久化存储。反序列化则是将字节流恢复为对象的过程。

    • 序列化

    对象状态转换:将对象的状态(即对象的字段值)转换为字节流。
    默认序列化机制:Java 提供了默认的序列化机制,通过 ObjectOutputStream 类的 writeObject 方法实现。
    自定义序列化:可以通过实现 writeObject 和 readObject 方法来自定义序列化和反序列化过程。

    • 持久化

    持久化:序列化的主要目的是将对象的状态保存到存储介质中,或者通过网络传输对象。

查询结果会被封装到 PageResult 对象中,其中 total 表示总记录数,records 表示当前页的数据集合。

sky-server  com/sky/mapper/EmployeeMapper.java
 /**
     * 分页查询 [动态sql 不用注解了 写道 EmployeeMapper.xml]
     * @param employeePageQueryDTO
     * @return
     */
    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
sky-server  mapper/EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.EmployeeMapper">
    <select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from employee
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>
</mapper>
<!--
and name like concat('%',#{name},'%'):
如果条件成立,生成的 SQL 条件为 AND name LIKE '%${name}%',实现名称的模糊匹配
-->

[员工管理] (http://localhost/#/employee)

代码完善
问题:创建/更新时间那边传入的数据不是想要的
// 2024929214237
"createTime": [
          2024,
          9,
          29,
          22,
          10,
          37
        ],
        "updateTime": [
          2024,
          9,
          29,
          22,
          10,
          37
        ],
解决方式:
  • 方法一:在属性上加注解,对日期进行格式化(只能处理单独一个属性)

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    ------------------------------------------------------------------------
     "createTime": "2024-09-29 22:10:37",
    
  • 方法二:在WebMvcConfiguration中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理

重写父类方法 去扩展 消息转换器

sky-server  com/sky/config/WebMvcConfiguration.java
 /**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中
        converters.add(0,converter);
    }
package com.sky.json;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

启用禁用员工账号

需求分析和设计
业务规则:
  • 可以对状态为 “启用” 的员工账号进行 “禁用” 操作
  • 可以对状态为 “禁用” 的员工账号进行 “启用” 操作
  • 状态为 “禁用” 的员工账号不能登录系统
sky-server
/**
     * 启用禁用员工账号
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用员工账号")
    //因为上面的和下面的参数都是一致 不然需要@PathVariable("status")解释一下
    public Result startOrStop(@PathVariable("status") Integer status, Long id) {
        log.info("启用禁用员工账号: {},{}",status,id);
        employeeService.startOrStop(status, id);
        return Result.success();
    }
sky-server  com/sky/service/EmployeeService.java
 /**
     * 启用禁用员工账号
     * @param status
     * @param id
     * @return
     */
    void startOrStop(Integer status, Long id);
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
/**
     * 启用禁用员工账号
     * @param status
     * @param id
     * @return
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        // update employee set status = ? where id = ?
        Employee employee = new Employee();
        employee.setStatus(status);
        employee.setId(id);

/** 要在Employee.java中添加@Builder 才能使用这种风格
 *      Employee employee = Employee.builder()
 *              .status(status)
 *              .id(id)
 *              .build();
 */
        employeeMapper.update(employee);
    }
sky-server  com/sky/mapper/EmployeeMapper.java
/**
     * 根据主键动态修改属性
     * @param employee
     */
    void update(Employee employee);
sky-server  mapper/EmployeeMapper.xml
<update id="update" parameterType="Employee">
        update employee
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="sex != null">sex = #{sex},</if>
            <if test="idNumber != null">id_Number = #{idNumber},</if>
            <if test="updateTime != null">update_Time = #{updateTime},</if>
            <if test="updateUser != null">update_User = #{updateUser},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        where id = #{id}
    </update>

编辑员工

需求分析和设计[回写数据]

编辑员工功能涉及到两个接口:
  • 根据id查询员工信息
  • 编辑员工信息

代码开发

sky-server  com/sky/controller/admin/EmployeeController.java
    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询员工信息")
    public Result<Employee> getById(@PathVariable Long id){
        Employee employee = employeeService.getById(id);
        return Result.success(employee);
    }

    /**
     * 编辑员工信息
     * @param employeeDTO
     * @return
     */
    @PutMapping
    @ApiOperation("编辑员工信息")
    public Result update(@RequestBody EmployeeDTO employeeDTO){
        log.info("编辑员工信息:{}", employeeDTO);
        employeeService.update(employeeDTO);
        return Result.success();
    }
sky-server  com/sky/service/EmployeeService.java
/**
     * 根据id查询员工
     * @param id
     * @return
     */
    Employee getById(Long id);

    /**
     * 编辑员工信息
     * @param employeeDTO
     */
    void update(EmployeeDTO employeeDTO);
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
/**
     * 根据id查询员工
     * @param id
     * @return
     */
    public Employee getById(Long id) {
        Employee employee = employeeMapper.getById(id);
        employee.setPassword("****");
        return employee;
    }

    /**
     * 编辑员工信息
     * @param employeeDTO
     */
    public void update(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        BeanUtils.copyProperties(employeeDTO, employee);

        //employee.setUpdateTime(LocalDateTime.now());
        //employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.update(employee);
    }
sky-server  com/sky/mapper/EmployeeMapper.java
 /**
     * 根据主键动态修改属性
     * @param employee
     */
    void update(Employee employee);

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @Select("select * from employee where id = #{id}")
    Employee getById(Long id);
  • 前端提交表单:
    用户在前端页面编辑员工信息并提交表单。
    表单数据被序列化为 JSON 格式,通过 HTTP PUT 请求发送到后端。
  • 后端接收数据:
    控制器方法 update 接收到 EmployeeDTO 对象。
    记录日志,输出接收到的员工信息。
    调用服务层的 update 方法,处理员工信息的更新。
  • 服务层处理:
    创建一个新的 Employee 对象。
    使用 BeanUtils.copyProperties 将 EmployeeDTO 的属性复制到 Employee 对象中。
    调用 MyBatis 的 employeeMapper,执行更新操作。
  • MyBatis 更新操作:
    生成动态 SQL 语句,只更新传入的非 null 属性。
    例如,如果 name 和 phone 不为 null,生成的 SQL 语句如下:

数据回写的具体过程

  • 前端请求获取员工信息 // 根据id查询员工信息
    当你点击编辑按钮时,前端会发起一个 HTTP GET 请求,从后端获取员工的详细信息。这些信息将被用来填充表单字段。
  • 后端处理 GET 请求
    后端需要提供一个接口来处理这个 GET 请求,并返回员工的详细信息。
  • 前端处理响应并填充表单
    前端接收到后端返回的员工信息后,将其填充到表单字段中
/**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("{/id}")
    @ApiOperation("根据id查询员工信息")
    public Result<Employee> getById(@PathVariable Long id) {
        log.info("根据id查询员工信息:{}", id);
        Employee employee = employeeService.getById(id);
        return Result.success(employee);
    }
<script>export default {
  data() {
    return {
      employee: {
        id: null,
        name: '',
        username: '',
        password: '',
        phone: '',
        sex: '',
        idNumber: ''
      }
    };
  },
  methods: {
    async fetchEmployee(id) {
      try {
        const response = await this.$axios.get(`/employees/${id}`);
        this.employee = response.data.data;
      } catch (error) {
        console.error('获取员工信息失败', error);
      }
    },
    async updateEmployee() {
      try {
        await this.$axios.put('/employees', this.employee);
        alert('员工信息更新成功');
      } catch (error) {
        console.error('更新员工信息失败', error);
      }
    }
  },
  mounted() {
    const id = this.$route.params.id; // 假设通过路由参数传递员工ID
    this.fetchEmployee(id);
  }
};
</script>
  • 提交表单
    当用户编辑完表单并点击保存按钮时,前端会发起一个 HTTP PUT 请求,将更新后的员工信息发送到后端进行处理

总结
前端请求获取员工信息:点击编辑按钮时,前端发起 GET 请求获取员工的详细信息。
后端处理 GET 请求:后端提供一个接口处理 GET 请求,返回员工的详细信息。
前端处理响应并填充表单:前端接收到员工信息后,将其填充到表单字段中。
提交表单:用户编辑完表单并点击保存按钮,前端发起 PUT 请求,将更新后的员工信息发送到后端进行处理。

[苍穹外卖项目接口文档] (http://localhost:8080/doc.html#/default/员工相关接口/updateUsingPUT)

导入分类管理功能代码

业务规则:
  • 分类名称必须是唯一
  • 分类按章类型可分为菜品分类套餐分类
  • 新添加的分类状态默认认为 “禁用
接口设计:
  • 新增分类
  • 分类分页查询
  • 根据id删除分类
  • 修改分类
  • 启用禁止分类
  • 根据类型调查分类

数据库设计(category表)

sky-server  com/sky/controller/admin/CategoryController.java
package com.sky.controller.admin;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
 * 分类管理
 */
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 新增分类
     * @param categoryDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增分类")
    public Result<String> save(@RequestBody CategoryDTO categoryDTO){
        log.info("新增分类:{}", categoryDTO);
        categoryService.save(categoryDTO);
        return Result.success();
    }

    /**
     * 分类分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分类分页查询")
    public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
        log.info("分页查询:{}", categoryPageQueryDTO);
        PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
        return Result.success(pageResult);
    }

    /**
     * 删除分类
     * @param id
     * @return
     */
    @DeleteMapping
    @ApiOperation("删除分类")
    public Result<String> deleteById(Long id){
        log.info("删除分类:{}", id);
        categoryService.deleteById(id);
        return Result.success();
    }

    /**
     * 修改分类
     * @param categoryDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改分类")
    public Result<String> update(@RequestBody CategoryDTO categoryDTO){
        categoryService.update(categoryDTO);
        return Result.success();
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用分类")
    public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
        categoryService.startOrStop(status,id);
        return Result.success();
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据类型查询分类")
    public Result<List<Category>> list(Integer type){
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}
sky-server  com/sky/service/CategoryService.java
package com.sky.service;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import java.util.List;

public interface CategoryService {

    /**
     * 新增分类
     * @param categoryDTO
     */
    void save(CategoryDTO categoryDTO);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    void deleteById(Long id);

    /**
     * 修改分类
     * @param categoryDTO
     */
    void update(CategoryDTO categoryDTO);

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}
sky-server  com/sky/service/impl/CategoryServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.CategoryMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 分类业务层
 */
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 新增分类
     * @param categoryDTO
     */
    public void save(CategoryDTO categoryDTO) {
        Category category = new Category();
        //属性拷贝
        BeanUtils.copyProperties(categoryDTO, category);

        //分类状态默认为禁用状态0
        category.setStatus(StatusConstant.DISABLE);

        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.insert(category);
    }

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
        PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
        //下一条sql进行分页,自动加入limit关键字分页
        Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 根据id删除分类
     * @param id
     */
    public void deleteById(Long id) {
        //查询当前分类是否关联了菜品,如果关联了就抛出业务异常
        Integer count = dishMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
        }

        //查询当前分类是否关联了套餐,如果关联了就抛出业务异常
        count = setmealMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
        }

        //删除分类数据
        categoryMapper.deleteById(id);
    }

    /**
     * 修改分类
     * @param categoryDTO
     */
    public void update(CategoryDTO categoryDTO) {
        Category category = new Category();
        BeanUtils.copyProperties(categoryDTO,category);

        //设置修改时间、修改人
        category.setUpdateTime(LocalDateTime.now());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.update(category);
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        Category category = Category.builder()
                .id(id)
                .status(status)
                .updateTime(LocalDateTime.now())
                .updateUser(BaseContext.getCurrentId())
                .build();
        categoryMapper.update(category);
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    public List<Category> list(Integer type) {
        return categoryMapper.list(type);
    }
}
sky-server  com/sky/mapper/CategoryMapper.java
package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.enumeration.OperationType;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface CategoryMapper {

    /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    void insert(Category category);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    @Delete("delete from category where id = #{id}")
    void deleteById(Long id);

    /**
     * 根据id修改分类
     * @param category
     */
    void update(Category category);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}
sky-server  mapper/CategoryMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.CategoryMapper">

    <select id="pageQuery" resultType="com.sky.entity.Category">
        select * from category
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
            <if test="type != null">
                and type = #{type}
            </if>
        </where>
        order by sort asc , create_time desc
    </select>

    <update id="update" parameterType="Category">
        update category
        <set>
            <if test="type != null">
                type = #{type},
            </if>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="sort != null">
                sort = #{sort},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>

    <select id="list" resultType="Category">
        select * from category
        where status = 1
        <if test="type != null">
            and type = #{type}
        </if>
        order by sort asc,create_time desc
    </select>
</mapper>
sky-server  com/sky/mapper/DishMapper.java
package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DishMapper {

    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

}
sky-server  com/sky/mapper/SetmealMapper.java
package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface SetmealMapper {

    /**
     * 根据分类id查询套餐的数量
     * @param id
     * @return
     */
    @Select("select count(id) from setmeal where category_id = #{categoryId}")
    Integer countByCategoryId(Long id);

}

菜品管理

公共字段自动填充

业务表中的公共字段:(后期会很多[菜品/套餐管理])

问题:代码冗余不利于后期维护
序号 字段名 含义 数据类型
1 create_time 创建时间 datetime
2 create_user 创建人id bigint
3 update_time 修改时间 datetime
4 update_user 修改人id bigint
解决:技术点 → 枚举、注解、AOP、反射
序号 字段名 含义 数据类型 操作类型
1 create_time 创建时间 datetime insert
2 create_user 创建人id bigint insert
3 update_time 修改时间 datetime insert、update
4 update_user 修改人id bigint insert、update
  • 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
  • 自定义切面 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
  • Mapper 的方法上加入 AutoFill 注解

代码开发1

sky-server  com/sky/annotation/AutoFill.java
package com.sky.annotation;

import com.sky.enumeration.OperationType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
// 枚举数据库操作类型:UPDATE INSERT [只要在这情况才有必要设置]
    OperationType value();
}
sky-server  com/sky/aspect/AutoFillAspect.java
package com.sky.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 切入点
     */
    // 拦截类 + 注解的东西
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    // 前置通知,在通知中进行公共字段的赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) {
        log.info("开始公共字段自动填充...");
    }
}
sky-server  com/sky/mapper/EmployeeMapper.java
//只在update和insert里加
@Mapper
public interface EmployeeMapper {
 /**
     * 插入员工数据
     * @param employee
     */
    @Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
            "values " +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    @AutoFill(value = OperationType.INSERT)
    void insert(Employee employee);
/**
     * 分页查询 [动态sql 不用注解了 写道 EmployeeMapper.xml]
     * @param employeePageQueryDTO
     * @return
     */
    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
    @AutoFill(value = OperationType.UPDATE)
}
sky-server  com/sky/mapper/CategoryMapper.java
package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.enumeration.OperationType;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface CategoryMapper {

    /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    void insert(Category category);

    /**
     * 根据id修改分类
     * @param category
     */
    void update(Category category);
}

详细讲解@AutoFill

@Target(ElementType.METHOD)
使用@Target注解指定自定义注解@AutoFill可以应用的目标元素类,这里指定了ElementType.METHOD,表示@AutoFill**只能应用于方法上**

@Retention(RetentionPolicy.RUNTIME)
使用 @Retention 注解指定自定义注解 @AutoFill 的保留策略。这里指定了 RetentionPolicy.RUNTIME,表示 @AutoFill 注解会在运行时保留,可以通过反射获取到。

public @interface AutoFill {…}
@interface:关键字,用于定义一个新的注解类型。
AutoFill:注解的名称,表示这个注解就叫做AutoFill

区分普通接口:@interface 与普通的 interface 不同,普通的interface用于定义接口,而@interface用于定义注解,@符号帮助编译器区分这两者

package com.sky.enumeration;

/**
 * 数据库操作类型
 */
public enum OperationType {
    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT
}

@Aspect
使用@Aspect注解将这个类标记为一个切面,切面是AOP(面向切面编程),用于定义切面关注点(日志记录、事务管理)

@Component
使用@Component注解将这个类标记为Spring管理的Bean,这样Spring容器会自动扫描并管理这个类的实例

@Slf4j
使用 @Slf4j 注解生成一个日志记录器(Logger)实例。这个注解来自 Lombok 库,可以简化日志记录器的创建

@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

使用@Pointcut注解定义一个切入点autoFillPointCut

  • execution(* com.sky.mapper.*.*(..)):匹配com.sky.mapper包下所有类的所有方法
  • && @annotation(com.sky.annotation.AutoFill):并且这些方法必须带有@AutoFill注解
  • public void autoFillPointCut():定义一个空的方法,用于标识这个切入点
/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 切入点
     */
    // 拦截类 + 注解的东西
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    // 前置通知,在通知中进行公共字段的赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) {
        log.info("开始公共字段自动填充...");
    }
}

使用@Before注解定义一个前置通知autoFill,这个通知会在切入点方法执行前被调用

  • @Before("autoFillPointCut()"):指定这个通知应用于autoFillPointCut切入点
  • public void autoFill(JoinPoint joinPoint):定义通知方法,接收一个JoinPoint参数,JoinPoint包含了连接点的信息,如被拦截的方法、参数等

代码开发2

公共属性赋值后 Service里的 save(Employee employee) → employee.setCreateUser(BaseContext.getCurrentId())就不用再去赋值了

这个写完后 就可以把Service里的一些employee.setXXX的删除了 因为公共属性只需要加@AutoFill
sky-server  com/sky/annotation/AutoFill.java 不变
sky-server  com/sky/aspect/AutoFillAspect.java
package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 切入点
     */
    // 拦截类 + 注解的东西
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    // 前置通知,在通知中进行公共字段的赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        log.info("开始公共字段自动填充...");

        // 获取当前被拦截的方法上的数据库操作类型(Update/Insert)
        MethodSignature signature = (MethodSignature)joinPoint.getSignature(); //方法签名对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); //获得方法上的注解对象
        OperationType operationType = autoFill.value();//获得数据库操作类型

        // 获取当当前被拦截的方法的参数--实体对象 (Employee employee)
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) { //没有参数不执行
            return;
        }
        Object entity = args[0]; //获得第一个

        // 准备赋值数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        // 根据当前不同的操作类型,对对应的属性通过反射来赋值
        if (operationType == OperationType.INSERT) {
            // 为4个公共字段赋值
            Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
            Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
            Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);


            //通过反射对对象赋值属性
            setCreateTime.invoke(entity,now);
            setCreateUser.invoke(entity,currentId);
            setUpdateTime.invoke(entity,now);
            setUpdateUser.invoke(entity,currentId);
        } else if (operationType == OperationType.UPDATE) {
            // 为2个公共字段赋值
            Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

            //通过反射对对象赋值属性
            setUpdateTime.invoke(entity,now);
            setUpdateUser.invoke(entity,currentId);
        }
    }
}

新增菜品

业务规则:
  • 菜品名称必须是唯一的
  • 菜品必须属于某个分类下,不能单独存在
  • 新增菜品时可以根据选择情况菜品的口味
  • 每个菜品必须对应一张图片
接口设计:
  • 根据类型查询分类(已完成) /admin/category/list GET

    这里要注意数据返回 因为它里面的口味算一个集合

  • 文件上传 /admin/common/upload POST

  • 新增菜品 /admin/dish POST

数据库设计:
  • dish菜品表 [一个菜品对应着多种口味]
  • dish_flavour口味表
开发文件上传接口:

浏览器 → 后端服务 → 阿里云OSS

sky-common  com/sky/utils/AliOssUtil.java
package com.sky.utils;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;


import java.io.ByteArrayInputStream;

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
// 通过配置类初始化这些数据
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

 // 创建OSSClient实例。 将字节数组转换为输入流,并将其上传到指定的bucket和objectName
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);
        log.info("文件上传成功,访问路径:{}", stringBuilder);

        return stringBuilder.toString();
    }
}
sky-common  com/sky/constant/AutoFillConstant.java
package com.sky.constant;

/**
 * 公共字段自动填充相关常量
 */
public class AutoFillConstant {
    /**
     * 实体类中的方法名称
     */
    public static final String SET_CREATE_TIME = "setCreateTime";
    public static final String SET_UPDATE_TIME = "setUpdateTime";
    public static final String SET_CREATE_USER = "setCreateUser";
    public static final String SET_UPDATE_USER = "setUpdateUser";
}
application-dev.yml
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    password: root
  alioss:
    endpoint: XXXXXXXXX
    access-key-id: XXXXXXXXXXXX
    access-key-secret: XXXXXXXXXXX
    bucketName: XXXXXXXXX
sky-server  com/sky/controller/admin/CommonController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.UUID;

/**
 * 通用接口
 */
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
    @Autowired
    private AliOssUtil aliOssUtil;
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    // 如果要测试文件上传 只能用postman或者前后端联调
    public Result<String> upload(MultipartFile file) {
        log.info("文件上传:{}", file);
        // 防止重名覆盖
        try {
            // 原始文件名
            String filename = file.getOriginalFilename();
            // 截取原始文件名的后缀
            String extension = filename.substring(filename.lastIndexOf("."));
            // 构造新文件名UUID
            String objectName = UUID.randomUUID().toString() + extension;

            // 文件的请求路径
            String filepath = aliOssUtil.upload(file.getBytes(), objectName);
            log.info("文件上传成功,访问路径:{}", filepath);
            return Result.success(filepath);
        } catch (IOException e) {
            log.error("文件上传失败:{}", e);
        }
        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

如何让application.yml识别到我在application-dev.yml里设置的值呢?

server:
  port: 8080

spring:
  profiles:
    active: dev

你已经在 application.yml 中指定了 spring.profiles.active: dev,这样在启动应用程序时,Spring Boot 会自动加载 application-dev.yml 中的配置

如果你使用的是 IDE(如 IntelliJ IDEA 或 Eclipse),你可以在运行配置中指定激活的环境配置文件。例如,在 IntelliJ IDEA 中:
打开 Run -> Edit Configurations。
选择你的应用程序配置。
在 VM options 中添加 -Dspring.profiles.active=dev。

# 在 VM options 中配置的原理:
在 VM options 中添加 -Dspring.profiles.active=dev 的原理是通过 Java 虚拟机(JVM)的系统属性来设置 Spring Boot 应用程序的活动配置文件。以下是详细的解释:
原理
JVM 系统属性:
JVM 提供了一种机制,允许你在启动时通过命令行参数传递系统属性。这些系统属性可以在应用程序中通过 System.getProperty 方法访问。
-D 前缀用于设置系统属性。例如,-Dkey=value 会将 key 设置为 value。
Spring Boot 配置:
Spring Boot 会读取 spring.profiles.active 系统属性来确定当前激活的配置文件。
当你通过 -Dspring.profiles.active=dev 设置系统属性时,Spring Boot 会在启动时读取这个属性,并根据其值加载相应的配置文件(如 application-dev.yml)。
新增菜品重要代码
sky-pojo  com/sky/dto/DishDTO.java
package com.sky.dto;

import com.sky.entity.DishFlavor;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDTO implements Serializable {
    //dish属性封装成dto

    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //口味[因为有多种口味要区分]
    private List<DishFlavor> flavors = new ArrayList<>();

}
sky-pojo  com/sky/entity/DishFlavor.java
package com.sky.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 菜品口味
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishFlavor implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    //菜品id
    private Long dishId;

    //口味名称
    private String name;

    //口味数据list
    private String value;

}
sky-server  com/sky/controller/admin/DishController.java
package com.sky.controller.admin;

import com.sky.dto.DishDTO;
import com.sky.result.Result;
import com.sky.service.DishService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
    @Autowired
    private DishService dishService;
    @PostMapping
    @ApiOperation("新增菜品")
    //@RequestBody 封装JSON格式的数据
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavour(dishDTO);
        return Result.success();
    }
}
com/sky/service/DishService.java
package com.sky.service;

import com.sky.dto.DishDTO;

public interface DishService {
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    public void saveWithFlavour(DishDTO dishDTO);
}
com/sky/service/impl/DishServiceImpl.java
package com.sky.service.impl;

import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Slf4j

public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    @Override
    @Transactional //保证事务一致性
    public void saveWithFlavour(DishDTO dishDTO) {
        Dish dish = new Dish();
        //直接new出来是空的需要先赋值 属性拷贝[属性命名要一致]
        BeanUtils.copyProperties(dishDTO,dish);

        // 向菜品表插入1条数据
        dishMapper.insert(dish);
        // 前端无法传 要获取dishId
// <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id"> 获取主键值
        Long dishId = dish.getId();

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            // 向口味表插入n条数据 集合对象批量传入不用集合
            dishFlavorMapper.insertBatch(flavors);
        }
    }
}
sky-server  com/sky/mapper/DishMapper.java
package com.sky.mapper;

import com.sky.annotation.AutoFill;
import com.sky.entity.Dish;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DishMapper {

    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

    /**
     * 插入菜品数据
     */
    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);
}
sky-server  mapper/DishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,
                          update_user, status)
        values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser},
                #{updateUser}, #{status})
    </insert>
</mapper>
sky-server  com/sky/mapper/DishFlavorMapper.java
package com.sky.mapper;

import com.sky.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface DishFlavorMapper {
    /**
     * 批量插入口味数据
     */
    void insertBatch(List<DishFlavor> flavors);
}
sky-server  mapper/DishFlavorMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">

    <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
        insert into dish_flavor (dish_id, name, value) values
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId},#{df.name},#{df.value})
        </foreach>
    </insert>
</mapper>

菜品分页查询

菜品名称[ ] 菜品分类[ ] 售卖状态[ ] [搜索]
菜品名称、图片、菜品分类、售价、售卖状态、最后操作事件、操作[修改 删除 启售,停售]
右下角 分页操作

业务规则:
  • 根据页码展示菜品信息
  • 每页展示10条数据
  • 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
接口设计:

Path:/admin/dish/page
Method:GET

代码开发:

根据菜品分页查询接口定义设计对应的DTO
根据菜品分页查询接口定义设计对应的VO[转成Json数据给前端]

sky-server  com/sky/controller/admin/DishController.java
@GetMapping("/page")
    @ApiOperation("菜品分页查询")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
        log.info("菜品分页查询:{}", dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }
sky-server  com/sky/service/DishService.java
package com.sky.service;

import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.result.PageResult;

public interface DishService {
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    public void saveWithFlavour(DishDTO dishDTO);

    /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
}
sky-server  com/sky/service/impl/DishServiceImpl.java
@Service
@Slf4j
public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
 @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
  // 1. 开启分页功能,设置当前页和每页显示的数量
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
 // 2. 调用 dishMapper 的 pageQuery 方法进行分页查询,返回一个 Page<DishVO> 对象
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
     // 3. 创建并返回 PageResult 对象,包含总记录数和查询结果列表
        return new PageResult(page.getTotal(), page.getResult());
    }
}
sky-server  com/sky/mapper/DishMapper.java
/**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
sky-server  mapper/DishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,
                          update_user, status)
        values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser},
                #{updateUser}, #{status})
    </insert>
    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.*, c.name as categoryName
        from dish d
            left join category c
                on d.category_id=c.id
        <where>
            <if test="name != null and name != ''">
                and d.name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and d.category_id = #{categoryId}
            </if>
            <if test="status != null">
                and d.status = #{status}
            </if>
        </where>
        order by d.update_time desc
    </select>
</mapper>

分页查询SQL语句分析

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

从 dish 表中选择所有列,并从 category 表中选择 name 列,别名为 categoryName。
使用左连接 (left join) 将 dish 表和 category 表连接起来,连接条件是 d.category_id = c.id。

动态生成WHERE子句,< where >标签会自动处理AND和OR关键字的添加,并且会忽略第一个条件前的ADN和OR

删除菜品

单个删除、批量删除、先停售后删除

业务规则:
  • 可以一次删除一个菜品,也可以批量删除菜品

Path: /admin/dish
Method: DELETE
数据库设计
dish表 → id 【菜品】
dish_flavor表 → dish_id 【口味】
setmeal_dish表 → dish_id

  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除
代码开发:
sky-server  com/sky/controller/admin/DishController.java
 /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除菜品")
    //@RequestParam MVC动态解析字符串 ids提取出来
    public Result delete(@RequestParam List<Long> ids) { //ids
        log.info("批量删除菜品:{}", ids);
        dishService.deleteBatch(ids);
        return Result.success();
    }
sky-server  com/sky/service/DishService.java
 /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    void deleteBatch(List<Long> ids);
sky-server  com/sky/service/impl/DishServiceImpl.java
/**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @Override
    public void deleteBatch(List<Long> ids) {
        // 判断当前菜品是否能够删除--是否存在起售中的菜品?? 取出id
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);
            if (dish.getStatus() == StatusConstant.ENABLE) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        // 判断当前菜品是否能够删除--是否被套餐关联了
        List<Long> setMealIds = setmealDishMapper.getSetmealIdsByDishId(ids);
        if (setMealIds != null && setMealIds.size() > 0) { //存在不允许删除
            // 当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }
        // 删除菜品表中的菜品数据
        for (Long id : ids) {
            dishMapper.deleteById(id);
            // 删除菜品关联的口味数据
            dishFlavorMapper.deleteByDishId(id);
        }
    }
sky-server  com/sky/mapper/DishMapper.java
/**
     * 根据主键删除菜品数据
     */

    @Delete("delete from dish where id = #{id}")
    void deleteById(Long id);
sky-server  com/sky/mapper/DishFlavorMapper.java
/**
     * 根据菜品id删除对应的 口味数据
     * @param id
     */
    @Delete("delete from dish_flavor where dish_id = #{id}")
    void deleteByDishId(Long id);
sky-server  com/sky/mapper/SetmealDishMapper.java
package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface SetmealDishMapper {
    /**
     * 根据菜品id查询对应的套餐id
     * @param dishIds
     * @return
     */
    // select setmeal_id from setmeal_dish where dish_id in (1,2,3)
    // 在mapper.xml中dishIds是形参  <foreach collection="dishIds">
    List<Long> getSetmealIdsByDishId(List<Long> dishIds);
}
sky-server  mapper/SetmealDishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealDishMapper">

    <select id="getSetmealIdsByDishId" resultType="java.lang.Long">
        SELECT setmeal_id
        FROM setmeal_dish
        WHERE dish_id IN
        <foreach collection="dishIds" item="dishId" open="(" separator="," close=")">
           #{dishId}
        </foreach>
    </select>
</mapper>

<!--
★ foreach 标签用于遍历集合,并生成相应的 SQL 语句。
★ collection="dishIds":指定要遍历的集合名称,即传入的参数 dishIds。
★ item="dishId":指定集合中的每个元素的别名,即每次迭代时的变量名。
★ separator=",":指定每个元素之间的分隔符,这里是逗号 ,。
★ open="(" 和 close=")":指定生成的 SQL 语句的开始和结束符号,这里是括号 ( 和 )。
-->

@RequestParm详细分析

public Result delete(@RequestParam List<Long> ids)

@RequestParam:注解用于将请求参数绑定到方法参数上。具体来说,它可以从请求的查询参数中提取出指定的参数值,并将其转换为方法参数的类型;在这个例子中,@RequestParam List ids 表示从请求的查询参数中提取 ids 参数,并将其转换为 List 类型。

修改菜品

数据回显

接口设计:

  • 根据id查询菜品

口味也要回显
Path: /admin/dish/{id}
Method:GET

  • 根据类型查询分类(已实现)
  • 文件上传(已实现)
  • 修改菜品

根据ID修改
Path:/admin/dish
Method:PUT

代码开发:

根据id查询菜品进行信息回显

sky-server  com/sky/controller/admin/DishController.java
/**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询菜品")
//  @PathVariable 注解用于从 URL 路径中的占位符参数中提取值,并将其绑定到方法参数上
    public Result<DishVO> getById(@PathVariable long id) {
        log.info("根据id查询菜品:{}", id);
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    }
sky-server  com/sky/service/DishService.java
 /**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    DishVO getByIdWithFlavor(long id);
sky-server  com/sky/service/impl/DishServiceImpl.java
/**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    @Override
    public DishVO getByIdWithFlavor(long id) {
        // 根据id查询菜品数据
        Dish dish = dishMapper.getById(id);
        // 根据菜品id查询口味数据
        List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);

        // 将查询到的数据封装到VO
        DishVO dishVO = new DishVO();
            // 属性拷贝
        BeanUtils.copyProperties(dish,dishVO);
        dishVO.setFlavors(dishFlavors);

        return dishVO;
    }
sky-server  com/sky/mapper/DishFlavorMapper.java
/**
     * 根据菜品id查询对应的口味数据
     * @param id
     * @return
     */
    @Select("select * from dish_flavor where dish_id = #{id}")
    List<DishFlavor> getByDishId(long id);

修改菜品接口

sky-server  com/sky/controller/admin/DishController.java
/**
     * 根据id修改菜品和对应的口味数据
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }
sky-server  com/sky/service/DishService.java
 /**
     * 根据id修改菜品和对应的口味数据
     * @param dishDTO
     * @return
     */
    void updateWithFlavor(DishDTO dishDTO);
sky-server  com/sky/service/impl/DishServiceImpl.java
/**
     * 根据id修改菜品和对应的口味数据
     * @param dishDTO
     * @return
     */
    @Override
    public void updateWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);

        // 修改菜品表基本信息 只是基础信息噢
        dishMapper.update(dish);
        // 先删掉原先的
        dishFlavorMapper.deleteByDishId(dishDTO.getId());
        // 再重新插入新的
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            // 向口味表插入n条数据 集合对象批量传入不用集合
            dishFlavorMapper.insertBatch(flavors);
        }
    }
/*
这段代码中,将 dishDTO 的属性复制到 dish 对象的主要原因有以下几点:
数据模型分离:
dishDTO 通常用于数据传输,包含前端传来的所有数据。
dish 是数据库实体类,只包含数据库表中的字段。
安全性:
使用 BeanUtils.copyProperties 可以避免将不必要的字段(如前端传来的额外属性)写入数据库。
确保只有预期的字段被更新。
数据校验:
dishDTO 可以包含更多的验证逻辑或额外的属性,而 dish 对象则严格遵循数据库模型。
通过这种方式,可以在更新前对数据进行进一步校验。
事务管理:
添加 @Transactional 注解确保整个更新过程在一个事务中完成。
如果任何一步出错,整个事务都会回滚,保证数据一致性。
*/
sky-server  com/sky/mapper/DishMapper.java
 /**
     * 根据id修改菜品和对应的口味数据
     * @param dish
     */
    //有时间和修改人 不要忘记自动填充
    @AutoFill(value = OperationType.UPDATE)
    void update(Dish dish);
sky-server  mapper/DishMapper.xml
<update id="update">
        update dish
        <set>
            <if
                test="name != null and name != ''">
                name = #{name},
            </if>
        </set>
        where id = #{id}
    </update>
菜品起售停售
sky-server  com/sky/controller/admin/DishController.java
/**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("菜品起售停售")
    public Result<String> startOrStop(@PathVariable Integer status, Long id) {
        dishService.startOrStop(status, id);
        return Result.success();
    }
sky-server  com/sky/service/DishService.java
/**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    void startOrStop(Integer status, Long id);
sky-server  com/sky/service/impl/DishServiceImpl.java
/**
     * 菜品起售停售
     * @param status
     * @param id
     */
@Override
    public void startOrStop(Integer status, Long id) {
        Dish dish = Dish.builder()
                .id(id)
                .status(status)
                .build();
        dishMapper.update(dish);

        if (status == StatusConstant.DISABLE) {
            // 如果是停售操作,还需要将包含当前菜品的套餐也停售
            List<Long> dishIds = new ArrayList<>();
            dishIds.add(id);
            // select setmeal_id from setmeal_dish where dish_id in (?,?,?)
            List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
            if (setmealIds != null && setmealIds.size() > 0) {
                for (Long setmealId : setmealIds) {
                    Setmeal setmeal = Setmeal.builder()
                            .id(setmealId)
                            .status(StatusConstant.DISABLE)
                            .build();
                    setmealMapper.update(setmeal);
                }
            }

        }
    }
sky-server  com/sky/mapper/SetmealMapper.java
/**
     * 根据id修改套餐
     *
     * @param setmeal
     */
    @AutoFill(OperationType.UPDATE)
    void update(Setmeal setmeal);
sky-server  com/sky/mapper/DishMapper.java

    /**
     * 根据套餐id查询菜品
     * @param setmealId
     * @return
     */
    @Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
    List<Dish> getBySetmealId(Long setmealId);
/*
在 SQL 查询中添加筛选条件。
确保返回的结果集中,setmeal_dish 表中的 setmeal_id 字段值与传入的 setmealId 参数值相匹配,从而获取与指定套餐 ID 相关的菜品列表。
*/
sky-server  mapper/SetmealMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealMapper">
    <resultMap id="setmealAndDishMap" type="com.sky.vo.SetmealVO" autoMapping="true">
        <result column="id" property="id"/>
        <collection property="setmealDishes" ofType="SetmealDish">
            <result column="sd_id" property="id"/>
            <result column="setmeal_id" property="setmealId"/>
            <result column="dish_id" property="dishId"/>
            <result column="sd_name" property="name"/>
            <result column="sd_price" property="price"/>
            <result column="copies" property="copies"/>
        </collection>
    </resultMap>
    <update id="update" parameterType="Setmeal">
        update setmeal
        <set>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="categoryId != null">
                category_id = #{categoryId},
            </if>
            <if test="price != null">
                price = #{price},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="description != null">
                description = #{description},
            </if>
            <if test="image != null">
                image = #{image},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>
</mapper>
@Mapper
public interface SetmealDishMapper {
    /**
     * 根据菜品id查询对应的套餐id
     * @param dishIds
     * @return
     */
    // select setmeal_id from setmeal_dish where dish_id in (1,2,3)
    // 在mapper.xml中dishIds是形参  <foreach collection="dishIds">
    List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
SetmealDishMapper.xml
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
        select setmeal_id from setmeal_dish where dish_id in
        <foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
            #{dishId}
        </foreach>
    </select>

修改套餐那些事

sky-server  com/sky/controller/admin/SetmealController.java
package com.sky.controller.admin;

import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.SetmealService;
import com.sky.vo.SetmealVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 套餐管理
 */
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    /**
     * 新增套餐
     *
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }

    /**
     * 分页查询
     *
     * @param setmealPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分页查询")
    public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }

    /**
     * 批量删除套餐
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result delete(@RequestParam List<Long> ids) {
        setmealService.deleteBatch(ids);
        return Result.success();
    }

    /**
     * 根据id查询套餐,用于修改页面回显数据
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询套餐")
    public Result<SetmealVO> getById(@PathVariable Long id) {
        SetmealVO setmealVO = setmealService.getByIdWithDish(id);
        return Result.success(setmealVO);
    }

    /**
     * 修改套餐
     *
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }

    /**
     * 套餐起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售停售")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result startOrStop(@PathVariable Integer status, Long id) {
        setmealService.startOrStop(status, id);
        return Result.success();
    }
}
sky-server  com/sky/service/SetmealService.java
package com.sky.service;

import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.result.PageResult;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;

import java.util.List;

public interface SetmealService {

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDTO
     */
    void saveWithDish(SetmealDTO setmealDTO);

    /**
     * 分页查询
     *
     * @param setmealPageQueryDTO
     * @return
     */
    PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

    /**
     * 批量删除套餐
     *
     * @param ids
     */
    void deleteBatch(List<Long> ids);

    /**
     * 根据id查询套餐和关联的菜品数据
     *
     * @param id
     * @return
     */
    SetmealVO getByIdWithDish(Long id);

    /**
     * 修改套餐
     *
     * @param setmealDTO
     */
    void update(SetmealDTO setmealDTO);

    /**
     * 套餐起售、停售
     *
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    List<DishItemVO> getDishItemById(Long id);
}
sky-server  com/sky/service/impl/SetmealServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.SetmealDish;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.exception.SetmealEnableFailedException;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 套餐业务实现
 */
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;


    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDTO
     */
    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //向套餐表插入数据
        setmealMapper.insert(setmeal);

        //获取生成的套餐id
        Long setmealId = setmeal.getId();

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });

        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
    }

    /**
     * 分页查询
     *
     * @param setmealPageQueryDTO
     * @return
     */
    public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
        int pageNum = setmealPageQueryDTO.getPage();
        int pageSize = setmealPageQueryDTO.getPageSize();

        PageHelper.startPage(pageNum, pageSize);
        Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 批量删除套餐
     *
     * @param ids
     */
    @Transactional
    public void deleteBatch(List<Long> ids) {
        ids.forEach(id -> {
            Setmeal setmeal = setmealMapper.getById(id);
            if (StatusConstant.ENABLE == setmeal.getStatus()) {
                //起售中的套餐不能删除
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
        });

        ids.forEach(setmealId -> {
            //删除套餐表中的数据
            setmealMapper.deleteById(setmealId);
            //删除套餐菜品关系表中的数据
            setmealDishMapper.deleteBySetmealId(setmealId);
        });
    }
/**
ids.forEach(id -> { ... }):对ids集合中的每个元素id执行大括号内的操作。
id -> { ... }:定义了一个接受单个参数id的函数,并执行大括号内的逻辑。
在大括号内,根据id查询数据库获取套餐信息,并检查其状态,若状态符合启用条件,则抛出异常。
**/
    
    /**
     * 根据id查询套餐和套餐菜品关系
     *
     * @param id
     * @return
     */
    public SetmealVO getByIdWithDish(Long id) {
        SetmealVO setmealVO = setmealMapper.getByIdWithDish(id);
        return setmealVO;
    }

    /**
     * 修改套餐
     *
     * @param setmealDTO
     */
    @Transactional
    public void update(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //1、修改套餐表,执行update
        setmealMapper.update(setmeal);

        //套餐id
        Long setmealId = setmealDTO.getId();

        //2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
        setmealDishMapper.deleteBySetmealId(setmealId);

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });
        //3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
        setmealDishMapper.insertBatch(setmealDishes);
    }

    /**
     * 套餐起售、停售
     *
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        //起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
        if (status == StatusConstant.ENABLE) {
            //select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
            List<Dish> dishList = dishMapper.getBySetmealId(id);
            if (dishList != null && dishList.size() > 0) {
                dishList.forEach(dish -> {
                    if (StatusConstant.DISABLE == dish.getStatus()) {
                        throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                    }
                });
            }
        }

        Setmeal setmeal = Setmeal.builder()
                .id(id)
                .status(status)
                .build();
        setmealMapper.update(setmeal);
    }

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    public List<Setmeal> list(Setmeal setmeal) {
        List<Setmeal> list = setmealMapper.list(setmeal);
        return list;
    }

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    public List<DishItemVO> getDishItemById(Long id) {
        return setmealMapper.getDishItemBySetmealId(id);
    }
}
sky-server  com/sky/mapper/SetmealMapper.java
package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.enumeration.OperationType;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;
import java.util.Map;

@Mapper
public interface SetmealMapper {

    /**
     * 根据分类id查询套餐的数量
     *
     * @param id
     * @return
     */
    @Select("select count(id) from setmeal where category_id = #{categoryId}")
    Integer countByCategoryId(Long id);

    /**
     * 根据id修改套餐
     *
     * @param setmeal
     */
    @AutoFill(OperationType.UPDATE)
    void update(Setmeal setmeal);

    /**
     * 新增套餐
     *
     * @param setmeal
     */
    @AutoFill(OperationType.INSERT)
    void insert(Setmeal setmeal);

    /**
     * 分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

    /**
     * 根据id查询套餐
     * @param id
     * @return
     */
    @Select("select * from setmeal where id = #{id}")
    Setmeal getById(Long id);

    /**
     * 根据id删除套餐
     * @param setmealId
     */
    @Delete("delete from setmeal where id = #{id}")
    void deleteById(Long setmealId);

    /**
     * 根据id查询套餐和套餐菜品关系
     * @param id
     * @return
     */
    SetmealVO getByIdWithDish(Long id);

    /**
     * 动态条件查询套餐
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据套餐id查询菜品选项
     * @param setmealId
     * @return
     */
    @Select("select sd.name, sd.copies, d.image, d.description " +
            "from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
            "where sd.setmeal_id = #{setmealId}")
    List<DishItemVO> getDishItemBySetmealId(Long setmealId);

    /**
     * 根据条件统计套餐数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
}
sky-server  mapper/SetmealMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealMapper">
    <resultMap id="setmealAndDishMap" type="com.sky.vo.SetmealVO" autoMapping="true">
        <result column="id" property="id"/>
        <collection property="setmealDishes" ofType="SetmealDish">
            <result column="sd_id" property="id"/>
            <result column="setmeal_id" property="setmealId"/>
            <result column="dish_id" property="dishId"/>
            <result column="sd_name" property="name"/>
            <result column="sd_price" property="price"/>
            <result column="copies" property="copies"/>
        </collection>
    </resultMap>
    <update id="update" parameterType="Setmeal">
        update setmeal
        <set>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="categoryId != null">
                category_id = #{categoryId},
            </if>
            <if test="price != null">
                price = #{price},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="description != null">
                description = #{description},
            </if>
            <if test="image != null">
                image = #{image},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>

<!--
    <insert>:表示这是一个插入操作。
    id="insert":指定这个 SQL 语句的唯一标识符,通常用于在 MyBatis 映射文件中引用此 SQL 语句。
    parameterType="Setmeal":指定插入操作的参数类型为 Setmeal 类型。
    useGeneratedKeys="true":指示 MyBatis 在执行插入操作后自动获取自动生成的主键。
    keyProperty="id":指定将自动生成的主键值设置到对象的 id 属性上。
-->
    <insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
        insert into setmeal
        (category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)
        values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},
                #{createUser}, #{updateUser})
    </insert>

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

    <select id="getByIdWithDish" parameterType="long" resultMap="setmealAndDishMap">
        select a.*,
               b.id    sd_id,
               b.setmeal_id,
               b.dish_id,
               b.name  sd_name,
               b.price sd_price,
               b.copies
        from setmeal a
                 left join
             setmeal_dish b
             on
                 a.id = b.setmeal_id
        where a.id = #{id}
    </select>

    <select id="list" parameterType="Setmeal" resultType="Setmeal">
        select * from setmeal
        <where>
            <if test="name != null">
                and name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
    </select>

    <select id="countByMap" resultType="java.lang.Integer">
        select count(id) from setmeal
        <where>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
        </where>
    </select>
</mapper>

Redis入门 [调整营业状态]

Redis是一个基于内存的 key-value 结构数据库

  • 基于内存存储,读写性能高
  • 适合存储热点数据 (热点商品、资讯、新闻) 访问量较大
  • 企业应用广泛

Redis常用数据类型

Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
  • 字符串 string:普通字符串
  • 哈希 hash:散列,类似于java中的HashMap结构
  • 列表 list:按照插入顺序排序,可以有重复元素,类似于java中的LinkedList
  • 集合 set:无序集合,没有重复元素,类似于java中的HashSet
  • 有序集合 sorted set / zset:集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素

Redis常用命令

  • 字符串操作命令

    ValueOperations valueOperations = redisTemplate.opsForValue();

    • SET key value 设置指定key的值
    • GET key 获取指定key的值
    • SETEX key seconds value 设置指定key的值,并将key的过期时间设为 seconds秒
    • SETNX key value 只有在key不存在时设置key的值
  • 哈希操作命令 [key → value(field1 value1, field2 value2)]

    HashOperations hashOperations = redisTemplate.opsForHash();

    Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象

    • HSET key field value 将哈希表key中的字段field的值设为value
    • HGET key field 获取存储在哈希表中指定字段的值
    • HDEL key field 删除存储在哈希表中的指定字段
    • HKEYS key 获取哈希表中所有字段
    • HVALS key 获取哈希表中所有值
  • 列表操作命令

    ListOperations listOperations = redisTemplate.opsForList();

    • LPUSH key value1 [value2] 将一个或多个值插入到列表头部
    • LRANGE key start stop 获取列表指定范围内的元素
    • RPOP key 移除并获取列表最后一个元素
    • LLEN key 获取列表长度
  • 集合操作命令

    SetOperations setOperations = redisTemplate.opsForSet();

    Redis set是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据

    • SADD key member1 [member2] 向集合添加一个或多个成员 [无序插入]
    • SMEMBERS key 返回集合中的所有成员
    • SCARD key 获取集合的成员数
    • SINTER key1 [key2] 返回给定所有集合的交集
    • SUNION key1 [key2] 返回所有给定集合的并集
    • SREM key member1 [member2] 删除集合中一个或多个成员
  • 有序列表操作命令

    ZSetOperations zSetOperations = redisTemplate.opsForZSet();

    Redis有序集合是string类型元素的集合,且不允许重复成员。每个元素都会关联一个double类型的分数

    • ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
    • ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
    • ZINCRBY key increment member 有序集合中对指定成员的分数加上增量increment
    • ZREM key member [member …] 移除有序集合中的一个或多个成员
  • 通用命令

    Redis的通用命令是不分数据类型的,都可以使用的命令

    • KEYS pattern 查找所有符合给定模式(pattern)的key
    • EXISTS key 检查给定key是否存在
    • TYPE key 返回key所存储的值的类型
    • DEL key 该命令用于在key存在是删除key

在java中操作Redis_SpringDataRedis

序列化器:redisTemplate.setKeySerializer(new StringRedisSerializer());

Redis的Java客户端很多
  • Jedis
  • Lettuce
  • Spring Data Redis

Spring Data Redis 是 Spring 的一部分,对Redis底层开发包进行了高度封装
在Spring项目中,可以使用Spring Data Redis来简化操作

Spring Data Redis
  • 导入Spring Data Redis的maven坐标

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.7.3</version>
    </dependency>
    
  • 配置Redis数据源

    spring:
     redis:
      host: localhost
      port: 6379
      password:
    
  • 编写配置类,创建RedisTemplate对象

    package com.sky.config;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    @Configuration
    @Slf4j
    public class RedisConfiguration {
    
        @Bean
        public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
            log.info("开始创建redis模板对象...");
            RedisTemplate redisTemplate = new RedisTemplate();
            //设置redis的连接工厂对象
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            //设置redis key的序列化器
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            return redisTemplate;
        }
    }
    
  • 通过RedisTemplate对象操作Redis

测试连接
application.yml
  redis:
    host: ${sky.redis.host}
    port: ${sky.redis.port}
    database: ${sky.redis.database}
application-dev.yml
  redis:
    host: localhost
    port: 6379
    database: 1
sky-server  com/sky/config/RedisConfiguration.java
package com.sky.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@Slf4j
public class RedisConfiguration {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器 在图形化界面不出现乱码
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}
sky-server【测试类】 com/sky/test/SpringDataRedisTest.java
package com.sky.test;

import com.mysql.cj.util.TimeUtil;
import net.sf.jsqlparser.statement.select.KSQLWindow;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;

import java.util.concurrent.TimeUnit;

@SpringBootTest //测试完记得注释 不然每次启动类就会运行这个测试类
public class SpringDataRedisTest {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testRedisTemplate(){
        System.out.println(redisTemplate);
        //创建根据字符串、哈希、列表、集合、有序列表、通用命令的代码
        ValueOperations valueOperations = redisTemplate.opsForValue();
        HashOperations hashOperations = redisTemplate.opsForHash();
        ListOperations listOperations = redisTemplate.opsForList();
        SetOperations setOperations = redisTemplate.opsForSet();
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    }

    /**
     * 操作字符串类型的数据
     */
    @Test
    public void testString(){
        // set get setex setnx
        redisTemplate.opsForValue().set("city","北京");
        String city = (String) redisTemplate.opsForValue().get("city");
        System.out.println(city); // 北京

        redisTemplate.opsForValue().set("code", "1234", 3, TimeUnit.MINUTES);
        // 第一次调用可以设置成功
        redisTemplate.opsForValue().setIfAbsent("lock", "1");
        // 第二次不可以成功
        redisTemplate.opsForValue().setIfAbsent("lock", "2");
        Object lock = redisTemplate.opsForValue().get("lock");
        System.out.println(lock); // 1

        Object lock2 = redisTemplate.opsForValue().get("locwwk");
        System.out.println(lock2);// null
    }
    
    /**
     * 操作哈希类型的数据
     */
    @Test
    public void testHash(){
        //hset hget hdel hkeys havls
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.put("100","name","tom");
        hashOperations.put("100","age","20");

        String name = (String) hashOperations.get("100","name");
        System.out.println(name);

        Set keys = hashOperations.keys("100");
        System.out.println(keys);

        List values = hashOperations.values("100");
        System.out.println(values);

        hashOperations.delete("100","age");
/**
     * 操作列表类型的数据
     */
    @Test
    public void testList(){
        //lpush lrange rpop llen
        ListOperations listOperations = redisTemplate.opsForList();

        listOperations.leftPushAll("mylist","a","b","c");
        listOperations.leftPush("mylist","d");

        List mylist = listOperations.range("mylist", 0, -1);
        System.out.println(mylist);

        listOperations.rightPop("mylist");

        Long size = listOperations.size("mylist");
        System.out.println(size);
    }

    /**
     * 操作集合类型的数据
     */
    @Test
    public void testSet(){
        //sadd smembers scard sinter sunion srem
        SetOperations setOperations = redisTemplate.opsForSet();

        setOperations.add("set1","a","b","c","d");
        setOperations.add("set2","a","b","x","y");

        Set members = setOperations.members("set1");
        System.out.println(members);

        Long size = setOperations.size("set1");
        System.out.println(size);

        Set intersect = setOperations.intersect("set1", "set2");
        System.out.println(intersect);

        Set union = setOperations.union("set1", "set2");
        System.out.println(union);

        setOperations.remove("set1","a","b");
    }

    /**
     * 操作有序集合类型的数据
     */
    @Test
    public void testZset(){
        //zadd zrange zincrby zrem
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

        zSetOperations.add("zset1","a",10);
        zSetOperations.add("zset1","b",12);
        zSetOperations.add("zset1","c",9);

        Set zset1 = zSetOperations.range("zset1", 0, -1);
        System.out.println(zset1);

        zSetOperations.incrementScore("zset1","c",10);

        zSetOperations.remove("zset1","a","b");
    }

    /**
     * 通用命令操作
     */
    @Test
    public void testCommon(){
        //keys exists type del
        Set keys = redisTemplate.keys("*");
        System.out.println(keys);

        Boolean name = redisTemplate.hasKey("name");
        Boolean set1 = redisTemplate.hasKey("set1");

        for (Object key : keys) {
            DataType type = redisTemplate.type(key);
            System.out.println(type.name());
        }

        redisTemplate.delete("mylist");
    }
}

店铺营业状态设置 【存入Redis】

接口设计:
  • 设置营业状态

    Path:/admin/shop/{status}
    Method:PUT
    status 1 店铺营业状态:1为营业,0为打样

  • 管理端查询营业状态

    Path:/admin/shop/status
    Method:GET

  • 用户端查询营业状态

    Path:/user/shop/status
    Method:GET

★ ★ 本项目约定 ★ ★

  • 管理端发出的请求,统一使用**/admin**作为前缀
  • 用户端发出的请求,统一使用**/user**作为前缀

营业状态数据存储方式:基于Redis的字符串来进行存储
key: SHOP_STATUS value: 1 1为营业,0为打样

代码开发:
sky-server  com/sky/controller/admin/ShopController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
    public static final String KEY = "SHOP_STATUS";

    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 设置店铺营业状态
     * @param status
     * @return
     */
    @PutMapping("/{status}") //动态取到status
    @ApiOperation("设置店铺营业状态")
    public Result setStatus(@PathVariable Integer status) {
        log.info("设置店铺的营业状态为:{}", status == 1 ? "营业中" : "打样中");
        redisTemplate.opsForValue().set(KEY, status);
        return Result.success();
    }

    /**
     * 获取店铺的营业状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺的营业状态")
    public Result<Integer> getStatus(){
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取到的店铺营业状态为:{}",status == 1 ? "营业中" : "打样中");
        return Result.success(status);
    }
}
sky-server  com/sky/controller/user/ShopController.java
package com.sky.controller.user;

import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
//@RestController("userShopController") 指定了这个控制器的名称为 userShopController
//这有助于在应用中唯一标识这个控制器,便于管理和调用
@RestController("userShopController")
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
    public static final String KEY = "SHOP_STATUS";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 获取店铺的营业状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺的营业状态")
    public Result<Integer> getStatus(){
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取到的店铺营业状态为:{}",status == 1 ? "营业中" : "打样中");
        return Result.success(status);
    }
}
sky-server  com/sky/config/WebMvcConfiguration.java
// 设置两个接口文档方便在前端文档处调试【管理端+用户端】
package com.sky.config;

import com.sky.interceptor.JwtTokenAdminInterceptor;
import com.sky.interceptor.JwtTokenUserInterceptor;
import com.sky.json.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.List;

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;

    /**
     * 注册自定义拦截器
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");

        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }

    @Bean
    public Docket docket1(){
        log.info("准备生成接口文档...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端接口")
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }

    @Bean
    public Docket docket2(){
        log.info("准备生成接口文档...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户端接口")
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }

    /**
     * 设置静态资源映射,主要是访问接口文档(html、js、css)
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始设置静态资源映射...");
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    /**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中
        converters.add(0,converter);
    }
}

回顾拦截器原理

HttpClient & 微信小程序开发

HttpClient

HttpClient 是 Apache Jakarta Common下的子项目,可以用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

核心API:

  • HttpClient
  • HttpClients
  • CloseableHttpClient
  • HttpGet
  • HttpPost

发送请求步骤:

  • 创建HttpClient对象
  • 创建Http请求对象
  • 调用HttpClient的execute方法发送请求
发送GET方式请求 [要先把项目跑起来]
sky-server  com/sky/test/HttpClientTest.java
package com.sky.test;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;

@SpringBootTest
public class HttpClientTest {
    /**
     * 测试通过httpclient发送GET方式的请求
     */

    @Test
    public void testGET() throws IOException {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 创建请求对象接口 (GET请求方式+请求地址)
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

        // 发送请求,接受响应结果
        CloseableHttpResponse response = httpClient.execute(httpGet);

        // 获取服务端返回的状态码
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("服务端返回的状态码为:" + statusCode);


        HttpEntity entity = response.getEntity();// 获得请求体
        String body = EntityUtils.toString(entity);
        System.out.println("服务端返回的数据为:" + body);

        // 关闭资源
        response.close();
        httpClient.close();
    }

    /**
     * 测试通过httpclient发送POST方式的请求
     */
}
--------------------------------------------------------------------------------
服务端返回的状态码为:200
服务端返回的数据为:{"code":1,"msg":null,"data":0}
发送POST方式请求 [要先把项目跑起来]
sky-server   com/sky/test/HttpClientTest.java
/**
     * 测试通过httpclient发送POST方式的请求
     */
    @Test
    public void testPOST() throws Exception{
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 创建请求对象
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
        // 以json方式请求提交参数
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");

        StringEntity entity = new StringEntity(jsonObject.toString());
        // 指定请求编码方式
        entity.setContentEncoding("utf-8");
        // 数据格式
        entity.setContentType("application/json");
        httpPost.setEntity(entity);

        // 发送请求
        CloseableHttpResponse response = httpClient.execute(httpPost);

        // 解析返回结果
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("响应码为:" + statusCode);

        HttpEntity entity1 = response.getEntity();
        String body = EntityUtils.toString(entity1);
        System.out.println("响应数据为:" + body);

        // 关闭资源
        response.close();
        httpClient.close();
    }
--------------------------------------------------------------------------------
响应码为:200
响应数据为:{"code":1,"msg":null,"data":{"id":1,"userName":"admin","name":"管理员","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzI4MTMyMzczfQ.8M2nIkgtHx8wpORNfhKEWjbprBV6OwC82wgYjAMxe2I"}}
封装后的HttpClientUtil
package com.sky.utils;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Http工具类
 */
public class HttpClientUtil {

    static final  int TIMEOUT_MSEC = 5 * 1000;

    /**
     * 发送GET方式请求
     * @param url
     * @param paramMap
     * @return
     */
    public static String doGet(String url,Map<String,String> paramMap){
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        String result = "";
        CloseableHttpResponse response = null;

        try{
            URIBuilder builder = new URIBuilder(url);
            if(paramMap != null){
                for (String key : paramMap.keySet()) {
                    builder.addParameter(key,paramMap.get(key));
                }
            }
            URI uri = builder.build();

            //创建GET请求
            HttpGet httpGet = new HttpGet(uri);

            //发送请求
            response = httpClient.execute(httpGet);

            //判断响应状态
            if(response.getStatusLine().getStatusCode() == 200){
                result = EntityUtils.toString(response.getEntity(),"UTF-8");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                response.close();
                httpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return result;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            // 创建参数列表
            if (paramMap != null) {
                List<NameValuePair> paramList = new ArrayList();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                // 模拟表单
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            if (paramMap != null) {
                //构造json格式数据
                JSONObject jsonObject = new JSONObject();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    jsonObject.put(param.getKey(),param.getValue());
                }
                StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
                //设置请求编码
                entity.setContentEncoding("utf-8");
                //设置数据类型
                entity.setContentType("application/json");
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }
    private static RequestConfig builderRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(TIMEOUT_MSEC)
                .setConnectionRequestTimeout(TIMEOUT_MSEC)
                .setSocketTimeout(TIMEOUT_MSEC).build();
    }
}

微信小程序开发

小程序 (qq.com)

详情 → 本地设置 → [取消勾选]不校验合法域名…

操作步骤
  • 了解小程序目录结构

    小程序包含一个描述整体程序app和多个和描述各自页面page,一个小程序主体部分由三个文件组村,必须放在项目的根目录

    文件 必需 作用
    app.js 小程序逻辑
    app.json 小程序公共配置
    app.wxss 小程序公共样式表
    一个小程序页面由四个文件组成 [pages → index → …]
    文件类型 必需 作用
    js 页面逻辑
    wxml 页面结构
    json 页面配置
    wxss 页面样式表
  • 编写测试小程序代码

    微信getUserProfile不弹出授权框_wx.getuserprofile没有弹窗-CSDN博客

    app.json 【外面一层】
    {
      "pages": [
        "pages/index/index",
        "pages/logs/logs"
      ],
      "window": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "Sky-Delivery",
        "navigationBarBackgroundColor": "#ffffff"
      },
      "style": "v2",
      "componentFramework": "glass-easel",
      "sitemapLocation": "sitemap.json",
      "lazyCodeLoading": "requiredComponents"
    }
    
    pages/index/index.wxml
    <view class="container">
      <view>
        {{msg}}
      </view>
    
      <view>
        <button bindtap="getUserInfo" type="primary">获取用户信息</button>
        昵称:{{nickName}}
        <image src="{{url}}" style="width: 200px;height: 200px;"></image>
        <button bindtap="wxLogin" type="warn">微信登录</button>
        授权码:{{code}}
      </view>
    
      <view>
        <button bindtap="sendRequest" type="default">发送请求</button>
      </view>
    </view>
    
    pages/index/index.js
    Page({
      data: {
        msg: 'hello world',
        nickName: '',
        url:'',
        code:'',
      },
    
      // 获取微信用户的头像和昵称
      getUserInfo(e){
        wx.getUserProfile({
          desc: '获取用户信息',
          success: (res) => {
            console.log(res.userInfo);
            // 为数据赋值
            this.setData({
              nickName: res.userInfo.nickName,
              url: res.userInfo.avatarUrl
            })
          },
          fail:(err) => {
            console.error('获取用户信息失败', err);
          }
        });
      },
      
      //微信登录,获取微信用户的授权码 
      //拿到后可以去请求微信服务器获得openId
      //授权码提交到后端去调用服务器
      wxLogin(){
        wx.login({
          success: (res) => {
            console.log(res.code)
            this.setData({
              code: res.code
            })
          }
        })
      },
    
      //发送请求
      sendRequest(){
        wx.request({
          url: 'http://localhost:8080/user/shop/status',
          method: 'GET',
          success: (res)=>{
            // data是后端响应回来的整个数据
            console.log(res.data)
          }
        })
      }
    });
    
  • 编译小程序

微信登录

导入小程序代码

E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day06\微信小程序代码\mp-weixin
【注意:导入后有很多包名错误common、components】

微信登录流程

开放能力 / 用户信息 / 小程序登录 (qq.com)

属性 类型 默认值 必填 说明
appid string 小程序 appId
secret string 小程序 appSecret
js_code string 登录时获取的 code
grant_type string 授权类型,此处只需填写 authorization_code

PostMan测试 →
GET:https://api.weixin.qq.com/sns/jscode2session?appid=wxa33b4bae9165c5a5&secret=c2d6fc237953d711146c4ad5db3ef947&js_code=0f1hdA200TsYYS1ghD100c3GZJ1hdA2w&grant_type=authorization_code

返回:
{“session_key”:”HsYD32ryqarcnrCXbEyWhg==”,”openid”:”obaex5N3w1_oAP6a4h-c-CkQBsZQ”}

需求分析和设计

数据库设计(user表)

代码开发
sky-server  application.yml
sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication
  alioss:
    endpoint: ${sky.alioss.endpoint}
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}
    bucket: ${sky.alioss.bucket}
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}
sky-server  application-dev.yml
  wechat:
    appid: xxxxxxx
    secret: xxxxxxx
sky-common  com/sky/properties/WeChatProperties.java
package com.sky.properties;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的秘钥
    private String mchid; //商户号
    private String mchSerialNo; //商户API证书的证书序列号
    private String privateKeyFilePath; //商户私钥文件
    private String apiV3Key; //证书解密的密钥
    private String weChatPayCertFilePath; //平台证书
    private String notifyUrl; //支付成功的回调地址
    private String refundNotifyUrl; //退款成功的回调地址
}
sky-server  com/sky/controller/user/UserController.java
package com.sky.controller.user;

import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.UserService;
import com.sky.utils.JwtUtil;
import com.sky.vo.UserLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private JwtProperties jwtProperties;
    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation("微信登录")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
        log.info("微信登录:{}", userLoginDTO.getCode());
        //微信登录
        User user = userService.wxLogin(userLoginDTO);

        //为微信用户生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID, user.getId());
        String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);
        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .openid(user.getOpenid())
                .token(token)
                .build();
        return Result.success(userLoginVO);
    }
}
sky-server  com/sky/service/UserService.java
package com.sky.service;

import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;

public interface UserService {
    /**
     * 微信登录
     * @return
     */
    User wxLogin(UserLoginDTO userLoginDTO);
}
sky-server  com/sky/service/impl/UserServiceImpl.java
package com.sky.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.constant.MessageConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.exception.LoginFailedException;
import com.sky.mapper.UserMapper;
import com.sky.properties.WeChatProperties;
import com.sky.service.UserService;
import com.sky.utils.HttpClientUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class UserServiceImpl implements UserService {
    // 微信服务接口地址
    public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

    @Autowired
    private WeChatProperties weChatProperties;
    @Autowired
    private UserMapper userMapper;
    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @Override
    public User wxLogin(UserLoginDTO userLoginDTO) {
        String openid = getOpenid(userLoginDTO.getCode());
        // 判断openId是否真的获取到 如果为空代表失败 业务异常
        if (openid == null){
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }
        // openId是否在表里 可判断是否为新用户
        User user = userMapper.getByOpenid(openid);
        // 如果是新用户,自动完成注册
        if (user == null) {
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            userMapper.insert(user);
        }
        // 返回这个用户对象
        return user;
    }

    /**
     * 调用微信接口服务,获取微信用户的openid
     * @param code
     * @return
     */
    //只有当前类用到
    private String getOpenid(String code) {
        // 调用微信服务器接口 获得当前用户的openid
        // 四个请求参数
        Map<String, String> map = new HashMap<>();
        map.put("appid", weChatProperties.getAppid());
        map.put("secret", weChatProperties.getSecret());
        map.put("js_code", code);
        map.put("grant_type", "authorization_code");
        String json = HttpClientUtil.doGet(WX_LOGIN, map);

        // 获得json对象
        JSONObject jsonObject = JSON.parseObject(json);
        String openid = jsonObject.getString("openid");
        return openid;
    }
}
sky-server  com/sky/mapper/UserMapper.java
package com.sky.mapper;

import com.sky.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserMapper {
    /**
     * 根据openid查询用户
     * @param openid
     * @return
     */
    @Select("select * from user where openid = #{openid}")
    User getByOpenid(String openid);

    /**
     * 新增用户
     * @param user
     */
    void insert(User user);
}
sky-server  mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.UserMapper">

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into user (openid, name, phone, sex, id_number, avatar, create_time)
        values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})
    </insert>
</mapper>
【检测小程序用户是否登陆性】
sky-server  com/sky/interceptor/JwtTokenUserInterceptor.java
package com.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前用户的id:", userId);
            BaseContext.setCurrentId(userId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
sky-server  com/sky/config/WebMvcConfiguration.java 【增加jwtTokenUserInterceptor】
/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;

    /**
     * 注册自定义拦截器
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");

        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }
}

导入商品浏览功能代码

接口设计
  • 查询分类

    Path: /user/category/list
    Method:GET
    请求参数
    Type: 分类类型→1.菜品分类 2.套餐分类

  • 根据分类id查询菜品

    Path: /user/dish/list
    Method:GET
    请求参数
    categoryId 分类id

  • 根据分类id查询套餐

    Path: /user/setmeal/list?category=111
    Method:GET
    请求参数
    categoryId 分类id

  • 根据套餐id查询包含的菜品

    Path: /user/setmeal/dish/10
    Method:GET
    请求参数
    id 套餐id
    返回数据:
    copies 份数
    description 菜品描述
    image 菜品图片
    name 菜品名称

sky-server  com/sky/controller/user/DishController.java
package com.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
    @Autowired
    private DishService dishService;

    /**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        List<DishVO> list = dishService.listWithFlavor(dish);

        return Result.success(list);
    }
}
sky-server  com/sky/service/DishService.java
package com.sky.service;

import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.vo.DishVO;

import java.util.List;

public interface DishService {
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    public void saveWithFlavour(DishDTO dishDTO);

    /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

    /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    void deleteBatch(List<Long> ids);

    /**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    DishVO getByIdWithFlavor(long id);

    /**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    void updateWithFlavor(DishDTO dishDTO);

    /**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    void startOrStop(Integer status, Long id);

    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    List<Dish> list(Long categoryId);

    /**
     * 条件查询菜品和口味
     * @param dish
     * @return
     */
    List<DishVO> listWithFlavor(Dish dish);
}
sky-server  com/sky/service/impl/DishServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.entity.Setmeal;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
@Slf4j

public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private SetmealMapper setmealMapper;
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    @Override
    @Transactional //保证事务一致性
    public void saveWithFlavour(DishDTO dishDTO) {
        Dish dish = new Dish();
        //直接new出来是空的需要先赋值 属性拷贝[属性命名要一致]
        BeanUtils.copyProperties(dishDTO,dish);

        // 向菜品表插入1条数据
        dishMapper.insert(dish);
        // 前端无法传 要获取dishId
// <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id"> 获取主键值
        Long dishId = dish.getId();

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            // 向口味表插入n条数据 集合对象批量传入不用集合
            dishFlavorMapper.insertBatch(flavors);
        }
    }

    /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @Transactional
    public void deleteBatch(List<Long> ids) {
        // 判断当前菜品是否能够删除--是否存在起售中的菜品?? 取出id
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);
            if (dish.getStatus() == StatusConstant.ENABLE) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        // 判断当前菜品是否能够删除--是否被套餐关联了
        List<Long> setMealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setMealIds != null && setMealIds.size() > 0) { //存在不允许删除
            // 当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }
        // 删除菜品表中的菜品数据
        for (Long id : ids) {
            dishMapper.deleteById(id);
            // 删除菜品关联的口味数据
            dishFlavorMapper.deleteByDishId(id);
        }
    }

    /**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    @Override
    public DishVO getByIdWithFlavor(long id) {
        // 根据id查询菜品数据
        Dish dish = dishMapper.getById(id);
        // 根据菜品id查询口味数据
        List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);

        // 将查询到的数据封装到VO
        DishVO dishVO = new DishVO();
            // 属性拷贝
        BeanUtils.copyProperties(dish,dishVO);
        dishVO.setFlavors(dishFlavors);

        return dishVO;
    }



    /**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    @Override
    public void updateWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);

        // 修改菜品表基本信息 只是基础信息噢
        dishMapper.update(dish);
        // 先删掉原先的
        dishFlavorMapper.deleteByDishId(dishDTO.getId());
        // 再重新插入新的
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            // 向口味表插入n条数据 集合对象批量传入不用集合
            dishFlavorMapper.insertBatch(flavors);
        }
    }

    /**
     * 菜品起售停售
     * @param status
     * @param id
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        Dish dish = Dish.builder()
                .id(id)
                .status(status)
                .build();
        dishMapper.update(dish);

        if (status == StatusConstant.DISABLE) {
            // 如果是停售操作,还需要将包含当前菜品的套餐也停售
            List<Long> dishIds = new ArrayList<>();
            dishIds.add(id);
            // select setmeal_id from setmeal_dish where dish_id in (?,?,?)
            List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
            if (setmealIds != null && setmealIds.size() > 0) {
                for (Long setmealId : setmealIds) {
                    Setmeal setmeal = Setmeal.builder()
                            .id(setmealId)
                            .status(StatusConstant.DISABLE)
                            .build();
                    setmealMapper.update(setmeal);
                }
            }

        }
    }

    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    public List<Dish> list(Long categoryId) {
        Dish dish = Dish.builder()
                .categoryId(categoryId)
                .status(StatusConstant.ENABLE)
                .build();
        return dishMapper.list(dish);
    }

    /**
     * 条件查询菜品和口味
     * @param dish
     * @return
     */
    public List<DishVO> listWithFlavor(Dish dish) {
        List<Dish> dishList = dishMapper.list(dish);

        List<DishVO> dishVOList = new ArrayList<>();

        for (Dish d : dishList) {
            DishVO dishVO = new DishVO();
            BeanUtils.copyProperties(d,dishVO);

            //根据菜品id查询对应的口味
            List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId());

            dishVO.setFlavors(flavors);
            dishVOList.add(dishVO);
        }

        return dishVOList;
    }
}

sky-server  com/sky/controller/user/SetmealController.java
package com.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Setmeal;
import com.sky.result.Result;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }

    /**
     * 根据套餐id查询包含的菜品列表
     *
     * @param id
     * @return
     */
    @GetMapping("/dish/{id}")
    @ApiOperation("根据套餐id查询包含的菜品列表")
    public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
        List<DishItemVO> list = setmealService.getDishItemById(id);
        return Result.success(list);
    }
}
sky-server  com/sky/service/SetmealService.java
package com.sky.service;

import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.result.PageResult;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;

import java.util.List;

public interface SetmealService {

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDTO
     */
    void saveWithDish(SetmealDTO setmealDTO);

    /**
     * 分页查询
     *
     * @param setmealPageQueryDTO
     * @return
     */
    PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

    /**
     * 批量删除套餐
     *
     * @param ids
     */
    void deleteBatch(List<Long> ids);

    /**
     * 根据id查询套餐和关联的菜品数据
     *
     * @param id
     * @return
     */
    SetmealVO getByIdWithDish(Long id);

    /**
     * 修改套餐
     *
     * @param setmealDTO
     */
    void update(SetmealDTO setmealDTO);

    /**
     * 套餐起售、停售
     *
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    List<DishItemVO> getDishItemById(Long id);
}
sky-server  com/sky/service/impl/SetmealServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.SetmealDish;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.exception.SetmealEnableFailedException;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 套餐业务实现
 */
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;


    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDTO
     */
    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //向套餐表插入数据
        setmealMapper.insert(setmeal);

        //获取生成的套餐id
        Long setmealId = setmeal.getId();

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });

        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
    }

    /**
     * 分页查询
     *
     * @param setmealPageQueryDTO
     * @return
     */
    public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
        int pageNum = setmealPageQueryDTO.getPage();
        int pageSize = setmealPageQueryDTO.getPageSize();

        PageHelper.startPage(pageNum, pageSize);
        Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 批量删除套餐
     *
     * @param ids
     */
    @Transactional
    public void deleteBatch(List<Long> ids) {
        ids.forEach(id -> {
            Setmeal setmeal = setmealMapper.getById(id);
            if (StatusConstant.ENABLE == setmeal.getStatus()) {
                //起售中的套餐不能删除
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
        });

        ids.forEach(setmealId -> {
            //删除套餐表中的数据
            setmealMapper.deleteById(setmealId);
            //删除套餐菜品关系表中的数据
            setmealDishMapper.deleteBySetmealId(setmealId);
        });
    }

    /**
     * 根据id查询套餐和套餐菜品关系
     *
     * @param id
     * @return
     */
    public SetmealVO getByIdWithDish(Long id) {
        SetmealVO setmealVO = setmealMapper.getByIdWithDish(id);
        return setmealVO;
    }

    /**
     * 修改套餐
     *
     * @param setmealDTO
     */
    @Transactional
    public void update(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //1、修改套餐表,执行update
        setmealMapper.update(setmeal);

        //套餐id
        Long setmealId = setmealDTO.getId();

        //2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
        setmealDishMapper.deleteBySetmealId(setmealId);

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });
        //3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
        setmealDishMapper.insertBatch(setmealDishes);
    }

    /**
     * 套餐起售、停售
     *
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        //起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
        if (status == StatusConstant.ENABLE) {
            //select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
            List<Dish> dishList = dishMapper.getBySetmealId(id);
            if (dishList != null && dishList.size() > 0) {
                dishList.forEach(dish -> {
                    if (StatusConstant.DISABLE == dish.getStatus()) {
                        throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                    }
                });
            }
        }

        Setmeal setmeal = Setmeal.builder()
                .id(id)
                .status(status)
                .build();
        setmealMapper.update(setmeal);
    }

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    public List<Setmeal> list(Setmeal setmeal) {
        List<Setmeal> list = setmealMapper.list(setmeal);
        return list;
    }

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    public List<DishItemVO> getDishItemById(Long id) {
        return setmealMapper.getDishItemBySetmealId(id);
    }
}

sky-server  com/sky/controller/user/CategoryController.java
package com.sky.controller.user;

import com.sky.entity.Category;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userCategoryController")
@RequestMapping("/user/category")
@Api(tags = "C端-分类接口")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查询分类")
    public Result<List<Category>> list(Integer type) {
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}
sky-server  com/sky/service/CategoryService.java
package com.sky.service;

import com.sky.annotation.AutoFill;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import java.util.List;

public interface CategoryService {

    /**
     * 新增分类
     * @param categoryDTO
     */
    void save(CategoryDTO categoryDTO);
    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    void deleteById(Long id);

    /**
     * 修改分类
     * @param categoryDTO
     */
    void update(CategoryDTO categoryDTO);

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}
sky-server  com/sky/service/impl/CategoryServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.CategoryMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 分类业务层
 */
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 新增分类
     * @param categoryDTO
     */
    public void save(CategoryDTO categoryDTO) {
        Category category = new Category();
        //属性拷贝
        BeanUtils.copyProperties(categoryDTO, category);

        //分类状态默认为禁用状态0
        category.setStatus(StatusConstant.DISABLE);
/**  公共属性
        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());
 **/

        categoryMapper.insert(category);
    }

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
        PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
        //下一条sql进行分页,自动加入limit关键字分页
        Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 根据id删除分类
     * @param id
     */
    public void deleteById(Long id) {
        //查询当前分类是否关联了菜品,如果关联了就抛出业务异常
        Integer count = dishMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
        }

        //查询当前分类是否关联了套餐,如果关联了就抛出业务异常
        count = setmealMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
        }

        //删除分类数据
        categoryMapper.deleteById(id);
    }

    /**
     * 修改分类
     * @param categoryDTO
     */
    public void update(CategoryDTO categoryDTO) {
        Category category = new Category();
        BeanUtils.copyProperties(categoryDTO,category);

        //设置修改时间、修改人 (公共属性)
//        category.setUpdateTime(LocalDateTime.now());
//        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.update(category);
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        Category category = Category.builder()
                .id(id)
                .status(status) // 下面注释是公共属性AOP有写
//                .updateTime(LocalDateTime.now())
//                .updateUser(BaseContext.getCurrentId())
                .build();
        categoryMapper.update(category);
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    public List<Category> list(Integer type) {
        return categoryMapper.list(type);
    }
}

缓存菜品 【redis】

问题说明:

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大

实现思路:

通过Redis来缓存菜品数据,减少数据库查询操作

开始→(查询菜品)→后端服务→缓存是否存在→(是)→读取缓存
(否)→查询数据库→载入缓存

缓存逻辑分析:
  • 每个分类下的菜品保存一份缓存数据

    key:dish_1
    value:string(…) [List集合]

sky-server com/sky/controller/user/DishController.java
package com.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {

        //构造redis中的key,规则:dish_分类id
        String key = "dish_" + categoryId;

        //查询redis中是否存在菜品数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
        if(list != null && list.size() > 0){
            //如果存在,直接返回,无须查询数据库
            return Result.success(list);
        }

        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        //如果不存在,查询数据库,将查询到的数据放入redis中
        list = dishService.listWithFlavor(dish);
        redisTemplate.opsForValue().set(key, list);

        return Result.success(list);
    }
}
清理缓存数据
防止 新增/更改/删除/起售停售 后无法及时在用户手机端接收

修改管理端接口 DishController 加入清理缓存的逻辑 (新增菜品、修改菜品、批量删除菜品、起售停售菜品)

sky-server  com/sky/controller/admin/DishController.java
/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate redisTemplate;
    @PostMapping
    @ApiOperation("新增菜品")
    //@RequestBody 封装JSON格式的数据
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavour(dishDTO);

        //清理缓存数据(精确查询)
        String key = "dish_" + dishDTO.getCategoryId();
        cleanCache(key);
        return Result.success();
    }
 /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除菜品")
    //@RequestParam MVC动态解析字符串 ids提取出来
    public Result delete(@RequestParam List<Long> ids) { //ids
        log.info("批量删除菜品:{}", ids);
        dishService.deleteBatch(ids);

        // 将所有的菜品缓存数据清理掉,所有的以dish_开头的key
        cleanCache("dish_*");
        return Result.success();
    }
/**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);

        // 将所有的菜品缓存数据清理掉,所有的以dish_开头的key
        cleanCache("dish_*");;
        return Result.success();
    }
/**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("菜品起售停售")
    public Result<String> startOrStop(@PathVariable Integer status, Long id) {
        dishService.startOrStop(status, id);

        // 将所有的菜品缓存数据清理掉,所有的以dish_开头的key
        cleanCache("dish_*");

        return Result.success();
    }
private void cleanCache(String pattern){
        /** 因为单独清理每个菜品可能会有关联套餐 就直接清理全部
         * 1. 先获取到所有的key
         * 2. 遍历key,判断是否以pattern开头
         * 3. 删除所有的key
         */
        Set keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }
}

缓存套餐 【SpringCache】

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现

  • EHCache
  • Caffeine
  • Redis
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Spring Cache
注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

     @CachePut(cacheNames = "userCache",key = "#user.id")
// 将方法的返回值放到缓存中
    // 如果使用Spring Cache缓存数据,key的生成"#user.id"
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    @DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

    @DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)
    public void deleteAll(){
        userMapper.deleteAll();
    }

    @GetMapping
    @Cacheable(cacheNames = "userCache",key = "#id")
// 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }
SpringCache入门案例
初始资源:
package com.itheima.controller;

import com.itheima.entity.User;
import com.itheima.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @PostMapping
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    @DeleteMapping
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

    @DeleteMapping("/delAll")
    public void deleteAll(){
        userMapper.deleteAll();
    }

    @GetMapping
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }

}
package com.itheima.mapper;

import com.itheima.entity.User;
import org.apache.ibatis.annotations.*;

@Mapper
public interface UserMapper{

    @Insert("insert into user(name,age) values (#{name},#{age})")
    @Options(useGeneratedKeys = true,keyProperty = "id")
    void insert(User user);

    @Delete("delete from user where id = #{id}")
    void deleteById(Long id);

    @Delete("delete from user")
    void deleteAll();

    @Select("select * from user where id = #{id}")
    User getById(Long id);
}
package com.itheima.entity;

import lombok.Data;
import java.io.Serializable;

@Data
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String name;

    private int age;

}
package com.itheima.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    /**
     * 生成接口文档配置
     * @return
     */
    @Bean
    public Docket docket(){
        log.info("准备生成接口文档...");

        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("接口文档")
                .version("2.0")
                .description("接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.itheima.controller"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }

    /**
     * 设置静态资源映射
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始设置静态资源映射...");
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}
application.yml
server:
  port: 8888
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/spring_cache_demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
  redis:
    host: localhost
    port: 6379
    database: 1
logging:
  level:
    com:
      itheima:
        mapper: debug
        service: info
        controller: info
springcachedemo.sql
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
);
pom.xml
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/>
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>springcache-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.1</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.7.3</version>
            </plugin>
        </plugins>
    </build>
</project>


开始调试咯
com/itheima/CacheDemoApplication.java
package com.itheima;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
//@EnableCaching 放在 Application 类上,这样整个应用就启用了缓存支持
@Slf4j
@SpringBootApplication
@EnableCaching
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
        log.info("项目启动成功...");
    }
}
com/itheima/controller/UserController.java
package com.itheima.controller;

import com.itheima.entity.User;
import com.itheima.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    /*
    set a:b:c:d: itheima 这个就是树形结构在Redis里面 文件夹包着文件夹
    */
    @Autowired
    private UserMapper userMapper;

    @PostMapping
//  @CachePut(cacheNames = "userCache",key = "#result.id") 对象导航
//  @CachePut(cacheNames = "userCache",key = "#p0.id")
//  @CachePut(cacheNames = "userCache",key = "#root.args[0]")
    @CachePut(cacheNames = "userCache",key = "#user.id")
// 将方法的返回值放到缓存中
    // 如果使用Spring Cache缓存数据,key的生成"#user.id"
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    @DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

    @DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)
    public void deleteAll(){
        userMapper.deleteAll();
    }


    @GetMapping
    @Cacheable(cacheNames = "userCache",key = "#id")
// 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }
}

缓存套餐_代码开发

实现思路
  • 导入 Spring CacheRedis 相关maven坐标
  • 启动类上加入 @EnableCaching 注解,开启缓存注解功能
  • 用户端接口 SetmealControllerlist 方法上加入 @Cacheable 注解
  • 管理端接口 SetmealControllersave、delete、update、startOrStop 等方法上
    加入**@CacheEvict** 注解保证数据一致性
com/sky/SkyApplication.java
package com.sky;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching //开启缓存注解
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}
sky-server  com/sky/controller/user/SetmealController.java
@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    @Cacheable(cacheNames = "setmealCache",key = "#categoryId") //key: setmealCache::100
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }
}
sky-server  com/sky/controller/admin/SetmealController.java

/**
 * 套餐管理
 */
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    /**
     * 新增套餐
     *
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
 /**
     * 批量删除套餐
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result delete(@RequestParam List<Long> ids) {
        setmealService.deleteBatch(ids);
        return Result.success();
    }
/**
     * 修改套餐
     *
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }

    /**
     * 套餐起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售停售")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result startOrStop(@PathVariable Integer status, Long id) {
        setmealService.startOrStop(status, id);
        return Result.success();
    }
}

添加购物车

套餐直接点击加号
菜品+ 或者有口味数据的选择后才可以加入购物车

接口设计:
  • 请求方式:POST
  • 请求路径:/user/shoppingCart/add
  • 请求参数:菜品id(dish_id)、口味(dish_flavor) 或 套餐id(setmeal_id) (JSON请求体)
  • 返回结果:code、data、msg
数据库设计(shopping_cart表):设置冗余字段可提高数据库效率
  • 作用:暂时存放所选商品的地方
  • 选的什么商品
  • 每个商品都买了几个
  • 不同用户的购物车需要区分开
sky-pojo  com/sky/dto/ShoppingCartDTO.java
package com.sky.dto;

import lombok.Data;
import java.io.Serializable;

@Data
public class ShoppingCartDTO implements Serializable {

    private Long dishId;
    private Long setmealId;
    private String dishFlavor;
}
sky-server  com/sky/controller/user/ShoppingCartController.java
package com.sky.controller.user;

import com.sky.dto.ShoppingCartDTO;
import com.sky.result.Result;
import com.sky.service.ShoppingCartService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "C端添加购物车接口")
public class ShoppingCartController {
    @Autowired
    private ShoppingCartService shoppingCartService;
    /**
     * 添加购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/add")
    @ApiOperation("添加购物车")
    public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
        shoppingCartService.addShoppingCart(shoppingCartDTO);
        return Result.success();
    }
}
sky-server  com/sky/service/ShoppingCartService.java
package com.sky.service;

import com.sky.dto.ShoppingCartDTO;
import org.springframework.stereotype.Service;

public interface ShoppingCartService {
    /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}
sky-server  com/sky/service/impl/ShoppingCartServiceImpl.java
package com.sky.service.impl;

import com.sky.context.BaseContext;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.ShoppingCart;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {
    /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;
    @Override
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        // 判断当前加入购物车中的商品是否已经存在了 (user_id + setmeal_id)
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        Long userId = BaseContext.getCurrentId();
        shoppingCart.setUserId(userId);

        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

        // 如果已经存在了,只需要将数量+1
        if (list != null && list.size() > 0) {
            ShoppingCart cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);
            // update shopping_cart set number = ? where id = ?
            shoppingCartMapper.updateNumberById(cart);
        } else {
            // 如果不存在,需要插入一条购物车数据
            // [先确定套餐or菜品]
            // 判断本次添加到购物车的是菜品还是套餐
            Long dishId = shoppingCartDTO.getDishId();
            if (dishId != null) {
                //本次添加到购物车的是菜品
                Dish dish = dishMapper.getById(dishId);
                shoppingCart.setName(dish.getName());
                shoppingCart.setImage(dish.getImage());
                shoppingCart.setAmount(dish.getPrice());
            } else {
                //本次添加到购物车的是套餐 查菜品表
                Long setmealId = shoppingCartDTO.getSetmealId();
                Setmeal setmeal = setmealMapper.getById(setmealId);
                shoppingCart.setName(setmeal.getName());
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());

            }
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(shoppingCart);
        }
    }
}
sky-server  com/sky/mapper/ShoppingCartMapper.java
package com.sky.mapper;

import com.sky.entity.ShoppingCart;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;

import java.util.List;

@Mapper
public interface ShoppingCartMapper {
    List<ShoppingCart> list(ShoppingCart shoppingCart);

    /**
     * 根据id修改商品数量
     * @param shoppingCart
     */
    @Update("update shopping_cart set number = #{number} where id = #{id}")
    void updateNumberById(ShoppingCart shoppingCart);

    /**
     * 插入购物车数据
     * @param shoppingCart
     */
    @Insert("insert into shopping_cart (name, user_id, dish_id, setmeal_id, dish_flavor, number, amount, image, create_time) " +
            " values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})")
    void insert(ShoppingCart shoppingCart);
}
sky-server  mapper/ShoppingCartMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.ShoppingCartMapper">

    <select id="list" resultType="com.sky.entity.ShoppingCart">
        select * from shopping_cart
        <where>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="dishId != null">
                and dish_id = #{dishId}
            </if>
            <if test="setmealId != null">
                and setmeal_id = #{setmealId}
            </if>
            <if test="dishFlavor != null">
                and dish_flavor = #{dishFlavor}
            </if>
        </where>
    </select>
</mapper>

删除购物车

sky-server  com/sky/controller/user/ShoppingCartController.java
/**
     * 删除购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/sub")
    @ApiOperation("删除购物车")
    public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("删除购物车,商品信息为:{}",shoppingCartDTO);
        shoppingCartService.subShoppingCart(shoppingCartDTO);
        return Result.success();
    }
sky-server  com/sky/service/ShoppingCartService.java
    /**
     * 删除购物车
     * @param shoppingCartDTO
     */
    void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
sky-server  com/sky/service/impl/ShoppingCartServiceImpl.java
/**
     * 删除购物车中的商品
     * @param shoppingCartDTO
     */
    @Override
    public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        shoppingCart.setUserId(userId);

        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        if (list != null && list.size() > 0) {
            ShoppingCart cart = list.get(0);
            if (cart.getNumber() > 1) {
                // 如果 number >1, 则需要将 number - 1
                cart.setNumber(cart.getNumber() - 1);
                shoppingCartMapper.updateNumberById(cart);
            } else {
                // 如果 number <=1,则直接删除该购物车数据
                shoppingCartMapper.deleteById(cart.getId());
            }
        }
    }
sky-server  com/sky/mapper/ShoppingCartMapper.java
/**
     * 根据id删除购物车数据
     * @param id
     */
    @Delete("delete from shopping_cart where id = #{id}")
    void deleteById(Long id);

查看购物车

名称、价格、商品、数量
Path:/user/shoppingCart/list
PUT

sky-server  com/sky/controller/user/ShoppingCartController.java
/**
     * 查看购物车
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查看购物车")
    public Result<List<ShoppingCart>> list(){
       List<ShoppingCart> list = shoppingCartService.showShoppingCart();
       return Result.success(list);
    }
sky-server  com/sky/service/ShoppingCartService.java
    /**
     * 查看购物车
     * @return
     */
    List<ShoppingCart> showShoppingCart();
sky-server  com/sky/service/impl/ShoppingCartServiceImpl.java
/**
     * 查看购物车
     * @return
     */
    @Override
    public List<ShoppingCart> showShoppingCart() {
        // 获取到当前微信用户的id
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = ShoppingCart.builder()
                .userId(userId)
                .build();
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        return list;
    }

清空购物车

Path:user/shoppingCart/clean
Method:DELETE
返回:code date msg

sky-server  com/sky/controller/user/ShoppingCartController.java
/**
     * 清空购物车
     * @return
     */
    @DeleteMapping("/clean")
    @ApiOperation("清空购物车")
    public Result clean() {
        shoppingCartService.cleanShoppingCart();
        return Result.success();
    }
sky-server  com/sky/service/ShoppingCartService.java
/**
     * 清空购物车 删除自己的购物车
     */
    void cleanShoppingCart();
sky-server  com/sky/service/impl/ShoppingCartServiceImpl.java
/**
     * 清空购物车
     */
    @Override
    public void cleanShoppingCart() {
        //获取到当前用户的id
        Long userId = BaseContext.getCurrentId();
        shoppingCartMapper.deleteByUserId(userId);
    }
sky-server  com/sky/mapper/ShoppingCartMapper.java
/**
     * 根据用户id清空购物车
     * @param userId
     */
    @Delete("delete from shopping_cart where user_id = #{userId}")
    void deleteByUserId(Long userId);

支付接口

导入地址簿功能代码

业务功能:

  • 查询地址列表

  • 新增地址

  • 修改地址

    Path:/user/addressBook
    Method:PUT

  • 删除地址

  • 设置默认地址

  • 查询默认地址

接口设计:

  • 新增地址

    Path: /user/addressBook
    Method: POST

  • 查询当前登录用户的所有地址信息

    Path: /user/addressBook/list
    Method: GET

  • 查询默认地址

  • 根据id修改地址

  • 根据id删除地址

    Path:/user/addressBook
    Method:DELETE

  • 根据id查询地址

    Path:/user/addressBook/{id}
    Method:GET

  • 设置默认地址

    Path:/user/addressBook/default
    Method:PUT

数据库设计(address_book表)

用户下单

在电商系统中,用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货

用户下单后会产生订单相关数据,订单数据需要体现信息:

  • 订单总金额是多少
  • 哪个用户下的单
  • 买的哪些商品
  • 每个商品数量是多少
  • 收货地址是哪
  • 用户手机号是多少

餐盒费:用数量算

用户下单接口设计

请求方式:POST
请求路径:/user/order/submit

参数:

  • 地址簿idaddressBookId
  • 配送状态(立即送出、选择送出时间)deliveryStatus
  • 打包费packAmount
  • 总金额amount
  • 备注remark
  • 餐具数量tablewareNumber
支付订单接口设计

返回数据:

  • 下单时间
  • 订单总金额
  • 订单号
  • 订单id
数据库设计订单表orders、订单明细表order_detail
  • 订单表 orders
    • 谁的订单?
    • 送哪去?
    • 打哪个电话联系?
    • 多少钱?
    • 什么时间下的单?
    • 什么时间支付的?
    • 订单的状态?
    • 订单号是多少?
  • 订单明细表 order_detail
    • 当前明细属于哪个订单?
    • 具体点的是什么商品?
    • 这个商品点了几份?

代码开发

用户下单1
根据用户下单接口的参数设计DTO:
sky-pojo  com/sky/dto/OrdersSubmitDTO.java
package com.sky.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class OrdersSubmitDTO implements Serializable {
    //地址簿id
    private Long addressBookId;
    //付款方式
    private int payMethod;
    //备注
    private String remark;
    //预计送达时间
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime estimatedDeliveryTime;
    //配送状态  1立即送出  0选择具体时间
    private Integer deliveryStatus;
    //餐具数量
    private Integer tablewareNumber;
    //餐具数量状态  1按餐量提供  0选择具体数量
    private Integer tablewareStatus;
    //打包费
    private Integer packAmount;
    //总金额
    private BigDecimal amount;
}
sky-pojo  com/sky/vo/OrdersSubmitVO.java
package com.sky.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {
    //订单id
    private Long id;
    //订单号
    private String orderNumber;
    //订单金额
    private BigDecimal orderAmount;
    //下单时间
    private LocalDateTime orderTime;
}
sky-server  com/sky/controller/user/OrderController.java
package com.sky.controller.user;

import com.sky.dto.OrdersSubmitDTO;
import com.sky.result.Result;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("userOrderController")
@RequestMapping("/user/order")
@Api(tags = "用户订单相关接口")
@Slf4j
public class OrderController {
    @Autowired
    private OrderService orderService;
    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @PostMapping("/submit")
    @ApiOperation("用户下单")
    public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
        log.info("用户下单,参数为:{}", ordersSubmitDTO);
        OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
        return Result.success(orderSubmitVO);
    }
}
sky-server  com/sky/service/OrderService.java
package com.sky.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {
    //订单id
    private Long id;
    //订单号
    private String orderNumber;
    //订单金额
    private BigDecimal orderAmount;
    //下单时间
    private LocalDateTime orderTime;
}
用户下单2
sky-pojo  com/sky/entity/AddressBook.java
package com.sky.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 地址簿
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //用户id
    private Long userId;

    //收货人
    private String consignee;

    //手机号
    private String phone;

    //性别 0 女 1 男
    private String sex;

    //省级区划编号
    private String provinceCode;

    //省级名称
    private String provinceName;

    //市级区划编号
    private String cityCode;

    //市级名称
    private String cityName;

    //区级区划编号
    private String districtCode;

    //区级名称
    private String districtName;

    //详细地址
    private String detail;

    //标签
    private String label;

    //是否默认 0否 1是
    private Integer isDefault;
}
sky-server  com/sky/service/impl/OrderServiceImpl.java
package com.sky.service.impl;

import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.entity.AddressBook;
import com.sky.entity.Orders;
import com.sky.entity.ShoppingCart;
import com.sky.exception.AddressBookBusinessException;
import com.sky.exception.ShoppingCartBusinessException;
import com.sky.mapper.AddressBookMapper;
import com.sky.mapper.OrderDetailMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @Override
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {

        // 1.处理各种业务异常(地址簿为空,购物车数据为空)
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
        if (addressBook == null) {
            // 抛出业务异常
            throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }
        // 查询当前用户购物车信息
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(userId);
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
        if (shoppingCartList == null || shoppingCartList.size() == 0) {
            // 抛出业务异常
            throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
        }

        // 2.向订单表插入1条数据
        Orders orders = new Orders();
        BeanUtils.copyProperties(ordersSubmitDTO, orders);
        orders.setOrderTime(LocalDateTime.now());
        orders.setPayStatus(Orders.UN_PAID);
        orders.setStatus(Orders.PENDING_PAYMENT);
        orders.setNumber(String.valueOf(System.currentTimeMillis()));//订单号
        orders.setPhone(addressBook.getPhone());
        orders.setConsignee(addressBook.getConsignee());
        orders.setUserId(userId);
        orderMapper.insert(orders);

        // 3.向订单明细表插入n条数据

        // 4,清空当前用户的购物车数据

        // 5.封装VO返回结果
        return null;
    }
}
sky-server  com/sky/mapper/OrderMapper.java
package com.sky.mapper;

import com.sky.entity.Orders;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper {
    /**
     * 用户下单
     * @param orders
     */
    void insert(Orders orders);
}
代码开发3
sky-server  com/sky/service/impl/OrderServiceImpl.java
package com.sky.service.impl;

import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.entity.AddressBook;
import com.sky.entity.OrderDetail;
import com.sky.entity.Orders;
import com.sky.entity.ShoppingCart;
import com.sky.exception.AddressBookBusinessException;
import com.sky.exception.ShoppingCartBusinessException;
import com.sky.mapper.AddressBookMapper;
import com.sky.mapper.OrderDetailMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @Transactional
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {

        // 1.处理各种业务异常(地址簿为空,购物车数据为空)
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
        if (addressBook == null) {
            // 抛出业务异常
            throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }
        // 查询当前用户购物车信息
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(userId);
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
        if (shoppingCartList == null || shoppingCartList.size() == 0) {
            // 抛出业务异常
            throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
        }

        // 2.向订单表插入1条数据
        Orders orders = new Orders();
        BeanUtils.copyProperties(ordersSubmitDTO, orders);
        orders.setOrderTime(LocalDateTime.now());
        orders.setPayStatus(Orders.UN_PAID);
        orders.setStatus(Orders.PENDING_PAYMENT);
        orders.setNumber(String.valueOf(System.currentTimeMillis()));//订单号
        orders.setPhone(addressBook.getPhone());
        orders.setConsignee(addressBook.getConsignee());
        orders.setUserId(userId);
        orderMapper.insert(orders);

        //批量插入订单明细数据
        List<OrderDetail> orderDetailList = new ArrayList<>();
        // 3.向订单明细表插入n条数据
        for (ShoppingCart cart : shoppingCartList) {
            OrderDetail orderDetail = new OrderDetail(); //订单明细
            BeanUtils.copyProperties(cart, orderDetail);
            orderDetail.setOrderId(orders.getId()); //设置当前订单明细关联的订单id
            orderDetailList.add(orderDetail);
        }
        orderDetailMapper.insertBatch(orderDetailList);
        // 4.清空当前用户的购物车数据
        shoppingCartMapper.deleteByUserId(userId);
        // 5.封装VO返回结果
        OrderSubmitVO ordersubmitVO = OrderSubmitVO.builder()
                .id(orders.getId())
                .orderNumber(orders.getNumber())
                .orderAmount(orders.getAmount())
                .orderTime(orders.getOrderTime())
                .build();
        return ordersubmitVO;
    }
}
sky-server  com/sky/mapper/OrderDetailMapper.java
package com.sky.mapper;

import com.sky.entity.OrderDetail;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface OrderDetailMapper {
    /**
     * 批量插入订单明细数据
     */
    void insertBatch(List<OrderDetail> orderDetailList);
}
sky-server  mapper/OrderDetailMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderDetailMapper">

    <insert id="insertBatch">
        insert into order_detail (name, image, order_id, dish_id, setmeal_id, dish_flavor, number, amount)
        values
        <foreach collection="orderDetailList" item="od" separator=",">
            (#{od.name},#{od.image},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},#{od.number},#{od.amount})
        </foreach>
    </insert>
</mapper>

订单支付

微信支付产品 + 微信支付

参考:https://pay.weixin.qq.com/static/product/product_index.shtml

微信支付接入流程:

提交资料 → 签署协议 → 绑定场景

微信小程序支付时序图:

JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单

请求URLhttps://api.mch.weixin.qq.com/v3/pay/transactions/jsapi

获取微信支付平台证书、商户私钥文件:

内网穿透工具

E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day08\安装包cpolar_amd64.msi

cpolar - secure introspectable tunnels to localhost
验证
你的隧道
[复制token] → 在cpolar文件里/cmd[C:\Program Files\cpolar] → cpolar.exe authtoken xxxx[Authtoken:这个是在网站验证里复制的] → cpolar.exe http 8080

cpolar by @bestexpresser (Ctrl+C to quit)

Tunnel Status online
Account Pluminary (Plan: Free)
Version 2.86.16/3.18
Web Interface 127.0.0.1:4042
Forwarding http://22d34b67.r9.cpolar.top -> http://localhost:8080
Forwarding https://22d34b67.r9.cpolar.top -> http://localhost:8080

Conn 0

Avg Conn Time 0.00ms

启动穿透地址:[22d34b67.r9.cpolar.top/doc.html] (https://22d34b67.r9.cpolar.top/doc.html)

此时正在下载资源
HTTP Requests


GET /v2/api-docs 200
GET /swagger-resources 200
GET /webjars/js/chunk-3b888a6 200
GET /webjars/js/chunk-589faee 200
GET /webjars/js/chunk-2d0bd79 200
GET /webjars/js/chunk-0fd6771 200
GET /webjars/js/chunk-0c58d94 200
GET /webjars/css/chunk-62d2fe 200
GET /webjars/js/app.0f2f48b5. 200

随后就可以访问到接口文档了!!
原理:使用内网穿透工具临时获得一个域名

CPolarSwitchHosts 是两种不同类型的软件,它们的功能和用途有所区别。

CPolar 是一款内网穿透软件,主要用于将本地运行的服务暴露到公网上,使得外网可以访问。它通过在本地和公网服务器之间建立一个安全的隧道,使得用户可以在任何地方通过互联网访问到本地的服务,比如网站、SSH、数据库等。

SwitchHosts 则是一款用于管理和切换本地hosts文件的软件。Hosts文件是操作系统用于将一些域名解析到特定的IP地址的一个文本文件。SwitchHosts 允许用户方便地添加、切换、备份不同的hosts规则,对于开发者来说,这在开发过程中进行域名映射和测试非常有用。

总结来说,CPolar主要用于内网穿透,而SwitchHosts用于hosts文件管理。两者解决的问题和适用场景不同,不是同一种软件。


内网、公网、外网和CPolar这几个概念在网络通信中扮演着不同的角色,以下是它们的定义和它们之间的联系:

  1. 内网(Local Network 或 Intranet): 内网是指一个私有网络,通常是在家庭、办公室或企业内部使用。内网中的设备通常通过路由器连接,并使用私有IP地址(如192.168.x.x或10.x.x.x)。内网中的设备一般不能直接从外部互联网访问,它们之间的通信受到防火墙和NAT(网络地址转换)的保护。
  2. 公网(Public Network 或 Internet): 公网是指全球范围内的开放网络,即互联网。公网上的设备使用公网IP地址,这些地址是全球唯一的,可以通过互联网被其他设备访问。网站、电子邮件服务器和其他在线服务都部署在公网上。
  3. 外网(External Network): 外网通常是指相对于内网而言的任何外部网络,特别是指互联网。当说“外网”时,通常是指从内网之外访问的资源或服务。
  4. CPolar: CPolar是一款内网穿透工具,它的主要作用是帮助内网中的设备暴露服务到公网上,使得这些服务可以被外网访问。以下是CPolar与内网、公网、外网之间的联系:
  • 内网到公网:CPolar在本地设备上运行一个客户端,该客户端与CPolar的服务器建立连接。当外部网络(公网)尝试访问CPolar服务器上配置的特定端口时,CPolar服务器会将这些请求转发到运行CPolar客户端的内网设备上。
  • 公网访问:通过CPolar,内网中的服务可以被赋予一个公网可访问的地址(通常是CPolar服务器的一个子域名或自定义域名),这样外网的任何用户都可以通过这个地址访问到内网的服务。

简而言之,CPolar是实现内网服务与公网之间通信的桥梁,它使得原本只能在局域网内部访问的服务能够被外网的用户访问。这对于远程工作、调试、以及需要在公网上提供服务的内网应用来说非常有用

导入功能代码【由于没有微信凭证 此接口未能正常开发 但代码均可学习】
sky-server  application-dev.yml
 wechat:
    appid: wxffb3637a228223b8
    secret: 84311df9199ecacdf4f12d27b6b9522d
    mchid : 1561414331
    mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
    privateKeyFilePath: D:\pay\apiclient_key.pem
    apiV3Key: CZBK51236435wxpay435434323FFDuv3
    weChatPayCertFilePath: D:\pay\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
    notifyUrl: https://58869fb.r2.cpolar.top/notify/paySuccess
    refundNotifyUrl: https://58869fb.r2.cpolar.top/notify/refundSuccess
sky-server  com/sky/controller/user/OrderController.java
/**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    @PutMapping("/payment")
    @ApiOperation("订单支付")
    public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        log.info("订单支付:{}", ordersPaymentDTO);
        OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
        log.info("生成预支付交易单:{}", orderPaymentVO);
        return Result.success(orderPaymentVO);
    }
sky-server  com/sky/service/OrderService.java
/**
     * 订单支付
     * @param ordersPaymentDTO
     * @return
     */
    OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;

    /**
     * 支付成功,修改订单状态
     * @param outTradeNo
     */
    void paySuccess(String outTradeNo);
sky-server  com/sky/service/impl/OrderServiceImpl.java
 @Autowired
    private UserMapper userMapper;
 @Autowired
    private WeChatPayUtil weChatPayUtil;

    
    /**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 查询订单
        Orders order = orderMapper.getByOrderNumber(ordersPaymentDTO.getOrderNumber());
        if (order == null) {
            throw new OrderBusinessException("订单不存在");
        }

        // 检查订单支付状态
        if (order.getPayStatus() == 1) { // 1 表示已支付
            throw new OrderBusinessException("该订单已支付");
        }
        order.setPayStatus(1);

        // 更新订单支付状态为已支付
        order.setPayStatus(Orders.PAID);
        order.setCheckoutTime(LocalDateTime.now());
        order.setPayMethod(ordersPaymentDTO.getPayMethod());
        order.setStatus(Orders.CONFIRMED);

        orderMapper.update(order);

        // 构造并返回支付结果对象
        OrderPaymentVO orderPaymentVO = new OrderPaymentVO();
        orderPaymentVO.setOrderNumber(order.getNumber()); // 订单号
        orderPaymentVO.setPaymentTime(new Date());
        orderPaymentVO.setPaymentStatus("SUCCESS");

        return orderPaymentVO;
    }
    /**
     * 支付成功,修改订单状态
     *
     * @param outTradeNo
     */
    public void paySuccess(String outTradeNo) {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();

        // 根据订单号查询当前用户的订单
        Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);

        // 根据订单id更新订单的状态、支付方式、支付状态、结账时间
        Orders orders = Orders.builder()
                .id(ordersDB.getId())
                .status(Orders.TO_BE_CONFIRMED)
                .payStatus(Orders.PAID)
                .checkoutTime(LocalDateTime.now())
                .build();

        orderMapper.update(orders);
    }
}
sky-server  com/sky/mapper/OrderMapper.java
package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.dto.GoodsSalesDTO;
import com.sky.dto.OrdersPageQueryDTO;
import com.sky.entity.Orders;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@Mapper
public interface OrderMapper {
    /**
     * 插入订单数据
     * @param order
     */
    void insert(Orders order);

    /**
     * 根据订单号和用户id查询订单
     * @param orderNumber
     * @param userId
     */
    @Select("select * from orders where number = #{orderNumber} and user_id= #{userId}")
    Orders getByNumberAndUserId(String orderNumber, Long userId);

    /**
     * 修改订单信息
     * @param orders
     */
    void update(Orders orders);
    /**
     * 根据id查询订单
     * @param id
     */
    @Select("select * from orders where id = #{id}}")
    Orders getById(Long id);
}
resources/mapper/OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">

    <insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
        insert into orders
        (number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
         phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
         tableware_status)
        values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
                #{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
                #{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
    </insert>

    <update id="update" parameterType="com.sky.entity.Orders">
        update orders
        <set>
            <if test="cancelReason != null and cancelReason!='' ">
                cancel_reason=#{cancelReason},
            </if>
            <if test="rejectionReason != null and rejectionReason!='' ">
                rejection_reason=#{rejectionReason},
            </if>
            <if test="cancelTime != null">
                cancel_time=#{cancelTime},
            </if>
            <if test="payStatus != null">
                pay_status=#{payStatus},
            </if>
            <if test="payMethod != null">
                pay_method=#{payMethod},
            </if>
            <if test="checkoutTime != null">
                checkout_time=#{checkoutTime},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="deliveryTime != null">
                delivery_time = #{deliveryTime}
            </if>
        </set>
        where id = #{id}
    </update>
        
        <!-- 根据订单号查询订单 -->
    <select id="getByOrderNumber" parameterType="String" resultType="Orders">
        select * from orders where number = #{orderNumber}
    </select>
</mapper>
sky-server  com/sky/mapper/UserMapper.java
  @Select("select * from user where id = #{id}}")
    User getById(Long userId);
sky-server  com/sky/controller/notify/PayNotifyController.java
package com.sky.controller.notify;

import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
//import com.sky.annotation.IgnoreToken;
import com.sky.properties.WeChatProperties;
import com.sky.service.OrderService;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;

/**
 * 支付回调相关接口
 */
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private WeChatProperties weChatProperties;

    /**
     * 支付成功回调
     *
     * @param request
     */
//  @IgnoreToken
    @RequestMapping("/paySuccess")
    public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //读取数据
        String body = readData(request);
        log.info("支付成功回调:{}", body);

        //数据解密
        String plainText = decryptData(body);
        log.info("解密后的文本:{}", plainText);

        JSONObject jsonObject = JSON.parseObject(plainText);
        String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
        String transactionId = jsonObject.getString("transaction_id");//微信支付交易号

        log.info("商户平台订单号:{}", outTradeNo);
        log.info("微信支付交易号:{}", transactionId);

        //业务处理,修改订单状态、来单提醒
        orderService.paySuccess(outTradeNo);

        //给微信响应
        responseToWeixin(response);
    }

    /**
     * 读取数据
     *
     * @param request
     * @return
     * @throws Exception
     */
    private String readData(HttpServletRequest request) throws Exception {
        BufferedReader reader = request.getReader();
        StringBuilder result = new StringBuilder();
        String line = null;
        while ((line = reader.readLine()) != null) {
            if (result.length() > 0) {
                result.append("\n");
            }
            result.append(line);
        }
        return result.toString();
    }

    /**
     * 数据解密
     *
     * @param body
     * @return
     * @throws Exception
     */
    private String decryptData(String body) throws Exception {
        JSONObject resultObject = JSON.parseObject(body);
        JSONObject resource = resultObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");

        AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //密文解密
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        return plainText;
    }

    /**
     * 给微信响应
     * @param response
     */
    private void responseToWeixin(HttpServletResponse response) throws Exception{
        response.setStatus(200);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("code", "SUCCESS");
        map.put("message", "SUCCESS");
        response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
        response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
    }
}

查询历史订单

业务规则

  • 分页查询历史订单
  • 可以根据订单状态查询
  • 展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)

接口设计:参见接口文档

1.2 代码实现

1.2.1 user/OrderController

    /**
     * 历史订单查询
     *
     * @param page
     * @param pageSize
     * @param status   订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
     * @return
     */
    @GetMapping("/historyOrders")
    @ApiOperation("历史订单查询")
    public Result<PageResult> page(int page, int pageSize, Integer status) {
        PageResult pageResult = orderService.pageQuery4User(page, pageSize, status);
        return Result.success(pageResult);
    }

1.2.2 OrderService

    /**
     * 用户端订单分页查询
     * @param page
     * @param pageSize
     * @param status
     * @return
     */
    PageResult pageQuery4User(int page, int pageSize, Integer status);

1.2.3 OrderServiceImpl

/**
     * 用户端订单分页查询
     *
     * @param pageNum
     * @param pageSize
     * @param status
     * @return
     */
    public PageResult pageQuery4User(int pageNum, int pageSize, Integer status) {
        // 设置分页
        PageHelper.startPage(pageNum, pageSize);

        OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
        ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());
        ordersPageQueryDTO.setStatus(status);

        // 分页条件查询
        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

        List<OrderVO> list = new ArrayList();

        // 查询出订单明细,并封装入OrderVO进行响应
        if (page != null && page.getTotal() > 0) {
            for (Orders orders : page) {
                Long orderId = orders.getId();// 订单id

                // 查询订单明细
                List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId);

                OrderVO orderVO = new OrderVO();
                BeanUtils.copyProperties(orders, orderVO);
                orderVO.setOrderDetailList(orderDetails);

                list.add(orderVO);
            }
        }
        return new PageResult(page.getTotal(), list);
    }

1.2.4 OrderMapper

    /**
     * 分页条件查询并按下单时间排序
     * @param ordersPageQueryDTO
     */
    Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);

1.2.5 OrderMapper.xml

    <select id="pageQuery" resultType="Orders">
        select * from orders
        <where>
            <if test="number != null and number!=''">
                and number like concat('%',#{number},'%')
            </if>
            <if test="phone != null and phone!=''">
                and phone like concat('%',#{phone},'%')
            </if>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="beginTime != null">
                and order_time &gt;= #{beginTime}
            </if>
            <if test="endTime != null">
                and order_time &lt;= #{endTime}
            </if>
        </where>
        order by order_time desc
    </select>

1.2.6 OrderDetailMapper

    /**
     * 根据订单id查询订单明细
     * @param orderId
     * @return
     */
    @Select("select * from order_detail where order_id = #{orderId}")
    List<OrderDetail> getByOrderId(Long orderId);

查询订单详情

2.2 代码实现

2.2.1 user/OrderController
    /**
     * 查询订单详情
     *
     * @param id
     * @return
     */
    @GetMapping("/orderDetail/{id}")
    @ApiOperation("查询订单详情")
    public Result<OrderVO> details(@PathVariable("id") Long id) {
        OrderVO orderVO = orderService.details(id);
        return Result.success(orderVO);
    }

2.2.2 OrderService

    /**
     * 查询订单详情
     * @param id
     * @return
     */
    OrderVO details(Long id);

2.2.3 OrderServiceImpl

    /**
     * 查询订单详情
     *
     * @param id
     * @return
     */
    public OrderVO details(Long id) {
        // 根据id查询订单
        Orders orders = orderMapper.getById(id);

        // 查询该订单对应的菜品/套餐明细
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

        // 将该订单及其详情封装到OrderVO并返回
        OrderVO orderVO = new OrderVO();
        BeanUtils.copyProperties(orders, orderVO);
        orderVO.setOrderDetailList(orderDetailList);

        return orderVO;
    }

2.2.4 OrderMapper

    /**
     * 根据id查询订单
     * @param id
     */
    @Select("select * from orders where id=#{id}")
    Orders getById(Long id);

取消订单

业务规则:

  • 待支付和待接单状态下,用户可直接取消订单
  • 商家已接单状态下,用户取消订单需电话沟通商家
  • 派送中状态下,用户取消订单需电话沟通商家
  • 如果在待接单状态下取消订单,需要给用户退款
  • 取消订单后需要将订单状态修改为“已取消”

3.2.1 user/OrderController

    /**
     * 用户取消订单
     *
     * @return
     */
    @PutMapping("/cancel/{id}")
    @ApiOperation("取消订单")
    public Result cancel(@PathVariable("id") Long id) throws Exception {
        orderService.userCancelById(id);
        return Result.success();
    }

3.2.2 OrderService

    /**
     * 用户取消订单
     * @param id
     */
    void userCancelById(Long id) throws Exception;

3.2.3 OrderServiceImpl

    /**
     * 用户取消订单
     *
     * @param id
     */
    public void userCancelById(Long id) throws Exception {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在
        if (ordersDB == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
        }

        //订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
        if (ordersDB.getStatus() > 2) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());

        // 订单处于待接单状态下取消,需要进行退款
        if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
            //调用微信支付退款接口
            weChatPayUtil.refund(
                    ordersDB.getNumber(), //商户订单号
                    ordersDB.getNumber(), //商户退款单号
                    new BigDecimal(0.01),//退款金额,单位 元
                    new BigDecimal(0.01));//原订单金额

            //支付状态修改为 退款
            orders.setPayStatus(Orders.REFUND);
        }

        // 更新订单状态、取消原因、取消时间
        orders.setStatus(Orders.CANCELLED);
        orders.setCancelReason("用户取消");
        orders.setCancelTime(LocalDateTime.now());
        orderMapper.update(orders);
    }

再来一单

4.2.1 user/OrderController

    /**
     * 再来一单
     *
     * @param id
     * @return
     */
    @PostMapping("/repetition/{id}")
    @ApiOperation("再来一单")
    public Result repetition(@PathVariable Long id) {
        orderService.repetition(id);
        return Result.success();
    }

4.2.2 OrderService

    /**
     * 再来一单
     *
     * @param id
     */
    void repetition(Long id);

4.2.3 OrderServiceImpl

    /**
     * 再来一单
     *
     * @param id
     */
   @Override
    public void repetition(Long id) {
        //查询当前用户id
        Long userId = BaseContext.getCurrentId();
        //根据订单id查询当前订单详情
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);

        // 将订单详情对象转换为购物车对象
        // 这一行使用 map 方法对每个 OrderDetail 对象进行转换操作,x 是当前遍历的 OrderDetail 对象
        List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
            //表示一个函数,该函数接受一个参数 x 并返回一个新对象
            ShoppingCart shoppingCart = new ShoppingCart();

            // 将原订单详情里面的菜品信息重新复制到购物车对象中
            BeanUtils.copyProperties(x, shoppingCart, "id");
            shoppingCart.setUserId(userId);
            shoppingCart.setCreateTime(LocalDateTime.now());

            return shoppingCart;
        }).collect(Collectors.toList());
// 使用 collect 方法将转换后的 ShoppingCart 对象收集到一个新的 List<ShoppingCart> 列表中
        // 将购物车对象批量添加到数据库
        shoppingCartMapper.insertBatch(shoppingCartList);
    }

4.2.4 ShoppingCartMapper

    /**
     * 批量插入购物车数据
     *
     * @param shoppingCartList
     */
    void insertBatch(List<ShoppingCart> shoppingCartList);

4.2.5 ShoppingCartMapper.xml

<insert id="insertBatch" parameterType="list">
        insert into shopping_cart
        (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
        values
        <foreach collection="shoppingCartList" item="sc" separator=",">
            
        </foreach>
</insert>

订单搜索

1.2.1 admin/OrderController

在admin包下创建OrderController

/**
 * 订单管理
 */
@RestController("adminOrderController")
@RequestMapping("/admin/order")
@Slf4j
@Api(tags = "订单管理接口")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 订单搜索
     *
     * @param ordersPageQueryDTO
     * @return
     */
    @GetMapping("/conditionSearch")
    @ApiOperation("订单搜索")
    public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
        PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO);
        return Result.success(pageResult);
    }
}

1.2.2 OrderService

    /**
     * 条件搜索订单
     * @param ordersPageQueryDTO
     * @return
     */
    PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO);

1.2.3 OrderServiceImpl

    /**
     * 订单搜索
     *
     * @param ordersPageQueryDTO
     * @return
     */
    public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
        PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());

        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

        // 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
        List<OrderVO> orderVOList = getOrderVOList(page);

        return new PageResult(page.getTotal(), orderVOList);
    }

    private List<OrderVO> getOrderVOList(Page<Orders> page) {
        // 需要返回订单菜品信息,自定义OrderVO响应结果
        List<OrderVO> orderVOList = new ArrayList<>();

        List<Orders> ordersList = page.getResult();
        if (!CollectionUtils.isEmpty(ordersList)) {
            for (Orders orders : ordersList) {
                // 将共同字段复制到OrderVO
                OrderVO orderVO = new OrderVO();
                BeanUtils.copyProperties(orders, orderVO);
                String orderDishes = getOrderDishesStr(orders);

                // 将订单菜品信息封装到orderVO中,并添加到orderVOList
                orderVO.setOrderDishes(orderDishes);
                orderVOList.add(orderVO);
            }
        }
        return orderVOList;
    }

    /**
     * 根据订单id获取菜品信息字符串
     *
     * @param orders
     * @return
     */
    private String getOrderDishesStr(Orders orders) {
        // 查询订单菜品详情信息(订单中的菜品和数量)
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

        // 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
        List<String> orderDishList = orderDetailList.stream().map(x -> {
            String orderDish = x.getName() + "*" + x.getNumber() + ";";
            return orderDish;
        }).collect(Collectors.toList());

        // 将该订单对应的所有菜品信息拼接在一起
        return String.join("", orderDishList);
    }

各个状态的订单数量统计

2.2.1 admin/OrderController

    /**
     * 各个状态的订单数量统计
     *
     * @return
     */
    @GetMapping("/statistics")
    @ApiOperation("各个状态的订单数量统计")
    public Result<OrderStatisticsVO> statistics() {
        OrderStatisticsVO orderStatisticsVO = orderService.statistics();
        return Result.success(orderStatisticsVO);
    }

2.2.2 OrderService

    /**
     * 各个状态的订单数量统计
     * @return
     */
    OrderStatisticsVO statistics();

2.2.3 OrderServiceImpl

    /**
     * 各个状态的订单数量统计
     *
     * @return
     */
    public OrderStatisticsVO statistics() {
        // 根据状态,分别查询出待接单、待派送、派送中的订单数量
        Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);
        Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);
        Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);

        // 将查询出的数据封装到orderStatisticsVO中响应
        OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();
        orderStatisticsVO.setToBeConfirmed(toBeConfirmed);
        orderStatisticsVO.setConfirmed(confirmed);
        orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);
        return orderStatisticsVO;
    }

2.2.4 OrderMapper

    /**
     * 根据状态统计订单数量
     * @param status
     */
    @Select("select count(id) from orders where status = #{status}")
    Integer countStatus(Integer status);

查询订单详情

业务规则:

  • 订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
  • 订单详情页面需要展示订单明细数据(商品名称、数量、单价)

3.2.1 admin/OrderController

    /**
     * 订单详情
     *
     * @param id
     * @return
     */
    @GetMapping("/details/{id}")
    @ApiOperation("查询订单详情")
    public Result<OrderVO> details(@PathVariable("id") Long id) {
        OrderVO orderVO = orderService.details(id);
        return Result.success(orderVO);
    }

接单

业务规则:

  • 商家接单其实就是将订单的状态修改为“已接单”

4.2.1 admin/OrderController

    /**
     * 接单
     *
     * @return
     */
    @PutMapping("/confirm")
    @ApiOperation("接单")
    public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
        orderService.confirm(ordersConfirmDTO);
        return Result.success();
    }

4.2.2 OrderService

    /**
     * 接单
     *
     * @param ordersConfirmDTO
     */
    void confirm(OrdersConfirmDTO ordersConfirmDTO);

4.2.3 OrderServiceImpl

    /**
     * 接单
     *
     * @param ordersConfirmDTO
     */
    public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
        Orders orders = Orders.builder()
                .id(ordersConfirmDTO.getId())
                .status(Orders.CONFIRMED)
                .build();

        orderMapper.update(orders);
    }

拒单

业务规则:

  • 商家拒单其实就是将订单状态修改为“已取消”
  • 只有订单处于“待接单”状态时可以执行拒单操作
  • 商家拒单时需要指定拒单原因
  • 商家拒单时,如果用户已经完成了支付,需要为用户退款

5.2.1 admin/OrderController

    /**
     * 拒单
     *
     * @return
     */
    @PutMapping("/rejection")
    @ApiOperation("拒单")
    public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
        orderService.rejection(ordersRejectionDTO);
        return Result.success();
    }

5.2.2 OrderService

    /**
     * 拒单
     *
     * @param ordersRejectionDTO
     */
    void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception;

5.2.3 OrderServiceImpl

    /**
     * 拒单
     *
     * @param ordersRejectionDTO
     */
    public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());

        // 订单只有存在且状态为2(待接单)才可以拒单
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        //支付状态
        Integer payStatus = ordersDB.getPayStatus();
        if (payStatus == Orders.PAID) {
            //用户已支付,需要退款
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
            log.info("申请退款:{}", refund);
        }

        // 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间
        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        orders.setStatus(Orders.CANCELLED);
        orders.setRejectionReason(ordersRejectionDTO.getRejectionReason());
        orders.setCancelTime(LocalDateTime.now());

        orderMapper.update(orders);
    }

取消订单

6.2 代码实现

6.2.1 admin/OrderController

    /**
     * 取消订单
     *
     * @return
     */
    @PutMapping("/cancel")
    @ApiOperation("取消订单")
    public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception {
        orderService.cancel(ordersCancelDTO);
        return Result.success();
    }

6.2.2 OrderService

    /**
     * 商家取消订单
     *
     * @param ordersCancelDTO
     */
    void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception;

6.2.3 OrderServiceImpl

    /**
     * 取消订单
     *
     * @param ordersCancelDTO
     */
    public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());

        //支付状态
        Integer payStatus = ordersDB.getPayStatus();
        if (payStatus == 1) {
            //用户已支付,需要退款
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
            log.info("申请退款:{}", refund);
        }

        // 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间
        Orders orders = new Orders();
        orders.setId(ordersCancelDTO.getId());
        orders.setStatus(Orders.CANCELLED);
        orders.setCancelReason(ordersCancelDTO.getCancelReason());
        orders.setCancelTime(LocalDateTime.now());
        orderMapper.update(orders);
    }

派送订单

业务规则:

  • 派送订单其实就是将订单状态修改为“派送中”
  • 只有状态为“待派送”的订单可以执行派送订单操作

7.2.1 admin/OrderController

    /**
     * 派送订单
     *
     * @return
     */
    @PutMapping("/delivery/{id}")
    @ApiOperation("派送订单")
    public Result delivery(@PathVariable("id") Long id) {
        orderService.delivery(id);
        return Result.success();
    }

7.2.2 OrderService

    /**
     * 派送订单
     *
     * @param id
     */
    void delivery(Long id);

7.2.3 OrderServiceImpl

    /**
     * 派送订单
     *
     * @param id
     */
    public void delivery(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在,并且状态为3
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        // 更新订单状态,状态转为派送中
        orders.setStatus(Orders.DELIVERY_IN_PROGRESS);

        orderMapper.update(orders);
    }

完成订单

业务规则:

  • 完成订单其实就是将订单状态修改为“已完成”
  • 只有状态为“派送中”的订单可以执行订单完成操作

8.2.1 admin/OrderController

    /**
     * 完成订单
     *
     * @return
     */
    @PutMapping("/complete/{id}")
    @ApiOperation("完成订单")
    public Result complete(@PathVariable("id") Long id) {
        orderService.complete(id);
        return Result.success();
    }

8.2.2 OrderService

    /**
     * 完成订单
     *
     * @param id
     */
    void complete(Long id);

8.2.3 OrderServiceImpl

    /**
     * 完成订单
     *
     * @param id
     */
    public void complete(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在,并且状态为4
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        // 更新订单状态,状态转为完成
        orders.setStatus(Orders.COMPLETED);
        orders.setDeliveryTime(LocalDateTime.now());

        orderMapper.update(orders);
    }

校验收货地址是否超出配送范围

1. 环境准备

注册账号:https://passport.baidu.com/v2/?reg&tt=1671699340600&overseas=&gid=CF954C2-A3D2-417F-9FE6-B0F249ED7E33&tpl=pp&u=https%3A%2F%2Flbsyun.baidu.com%2Findex.php%3Ftitle%3D%E9%A6%96%E9%A1%B5

登录百度地图开放平台:https://lbsyun.baidu.com/

进入控制台,创建应用,获取AK:

![image-20221222170049729](E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day09\项目实战参考答案\assets\image-20221222170049729.png)

![image-20221222170256927](E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day09\项目实战参考答案\assets\image-20221222170256927.png)

相关接口:

https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding

https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1

2. 代码开发

2.1 application.yml

配置外卖商家店铺地址和百度地图的AK:

![image-20221222170819582](E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day09\项目实战参考答案\assets\image-20221222170819582.png)

2.2 OrderServiceImpl

改造OrderServiceImpl,注入上面的配置项:

com/sky/properties/BaiDuProperties.java
package com.sky.properties;

import lombok.Data;
import lombok.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
// 多个prefix
@ConfigurationProperties(prefix = "sky.baidu")
@Data
public class BaiDuProperties {
    private String shopAddress;
    private String ak;
}
application.yml
  baidu:
    ak: ${sky.baidu.ak}
    shopAddress: ${sky.baidu.shopAddress}
application-dev.yml
  baidu:
    ak: xxxxxxxxx
    shopAddress: 河北省唐山市丰润区燕山路街道美景花园

在OrderServiceImpl中提供校验方法:

/**
     * 检查客户的收货地址是否超出配送范围
     * @param address
     */
    private void checkOutOfRange(String address) {
        Map<String, String> map = new HashMap<>();
        map.put("address", baiDuProperties.getShopAddress());
        map.put("output", "json");
        map.put("ak", baiDuProperties.getAk());

        //获取店铺的经纬度坐标
        String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);

        JSONObject jsonObject = JSON.parseObject(shopCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("店铺地址解析失败");
        }

        //数据解析
        JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
        String lat = location.getString("lat");
        String lng = location.getString("lng");
        //店铺经纬度坐标
        String shopLngLat = lat + "," + lng;

        map.put("address",address);
        //获取用户收货地址的经纬度坐标
        String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);

        jsonObject = JSON.parseObject(userCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("收货地址解析失败");
        }

        //数据解析
        location = jsonObject.getJSONObject("result").getJSONObject("location");
        lat = location.getString("lat");
        lng = location.getString("lng");
        //用户收货地址经纬度坐标
        String userLngLat = lat + "," + lng;

        map.put("origin",shopLngLat);
        map.put("destination",userLngLat);
        map.put("steps_info","0");

        //路线规划
        String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map);

        jsonObject = JSON.parseObject(json);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("配送路线规划失败");
        }

        //数据解析
        JSONObject result = jsonObject.getJSONObject("result");
        JSONArray jsonArray = (JSONArray) result.get("routes");
        Integer distance = (Integer) ((JSONObject) jsonArray.get(0)).get("distance");

        if(distance > 5000){
            //配送距离超过5000米
            throw new OrderBusinessException("超出配送范围");
        }
    }

在OrderServiceImpl的submitOrder方法中调用上面的校验方法:

   // 检查用户的收获地址是否超出配送范围
        checkOutOfRange(addressBook.getCityName() + addressBook.getDistrictName() + addressBook.getDetail());

![image-20221222171444981](E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day09\项目实战参考答案\assets\image-20221222171444981.png)

SpringTask[定时任务]定时自动执行某段Java代码

SpringTask是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑

应用场景
  • 信用卡每月还款提醒
  • 银行贷款每月还款提醒
  • 火车票售票系统处理未支付订单
  • 入职纪念日为用户发送通知
cron表达式

cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

2022年10月12日上午9点整 对应的cron表达式(日 和 周 不能同时定义)

0 0 9 12 10 ? 2022
https://cron.qqe2.com

分钟 小时
0 0 9 12 10
SpringTask使用步骤:
  • 导入maven坐标 spring-context(已存在)
  • 启动类添加注解 @EnableScheduling 开启任务调度
  • 自定义定时任务类
sky-server  com/sky/task/MyTask.java
package com.sky.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;

/**
 * 自定义定时任务类
 */
@Component
@Slf4j
public class MyTask {

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

订单状态定时处理

用户下单后可能存在的情况:
  • 下单后未支付,订单一直处于”待支付“状态
  • 用户收获后管理端未点击完成按钮,订单一直处于“派送中”状态
    • 通过定时任务每分钟检查一次是否存在支付超时订单(超过15min),如果存在则修改订单状态为”已取消”
    • 通过定时任务每天凌晨1点检查一次是否存在”派送中”的订单,如果存在则修改订单状态为”已完成”
代码开发:
sky-server  com/sky/task/OrderTask.java
package com.sky.task;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

@Component
@Slf4j
public class OrderTask {
    @Autowired
    private OrderMapper orderMapper;

    /**
     * 处理超时订单的方法
     */
    @Scheduled(cron = "0 * * * * ?")//每分钟触发一次
    public void processTimeoutOrder(){
        log.info("定时处理超时订单");
        // select * from orders where status = ? and order_time = (当前时间 - 15分钟)
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
        if (ordersList != null && ordersList.size() > 0) {
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.CANCELLED);
                orders.setCancelReason("订单超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }
    }

    /**
     * 处理一直处于派送中状态的订单
     */
    @Scheduled(cron = "0 0 1 * * ?")//每天凌晨一点
    public void processDeliveryOrder() {
        log.info("定时处理处于派送中的订单");
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
        if (ordersList != null && ordersList.size() > 0) {
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }
    }
}
sky-server  com/sky/mapper/OrderMapper.java
    /**
     *
     * 根据订单状态和下单时间查询订单
     * @param status
     * @param orderTime
     * @return
     */
    @Select("select * from orders where status = #{status} and order_time < #{orderTime}")
    List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);

WebSocket

WebSocket是基于TCP的一种新的网络协议,它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持续性连接,并进行双向数据传输

应用场景:
  • 视频弹幕
  • 网页聊天
  • 体育实况更新
  • 股票基金报价实时更新
入门案例

实现步骤:

  • 直接使用websocket.html页面坐位WebSocket客户端
  • 导入WebSocket的maven坐标
  • 导入WebSocket服务端组件WebSocketServer,用于和客户端通信
  • 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
  • 导入定时人物类WebSocketTask,定时向客户端推送数据
websocket.html
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Demo</title>
</head>
<body>
    <input id="text" type="text" />
    <button onclick="send()">发送消息</button>
    <button onclick="closeWebSocket()">关闭连接</button>
    <div id="message">
    </div>
</body>
<script type="text/javascript">
    var websocket = null;
    var clientId = Math.random().toString(36).substr(2);

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        //连接WebSocket节点
        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    }
    else{
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(){
        setMessageInnerHTML("连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //发送消息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
    
    //关闭连接
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>
com/sky/websocket/WebSocketServer.java
package com.sky.websocket;

import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * WebSocket服务
 */
@Component //交给spring容器管理
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发
     *
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
com/sky/config/WebSocketConfiguration.java
package com.sky.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
com/sky/task/WebSocketTask.java
package com.sky.task;

import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
    }
}

来单提醒

用户下单并且支付成功后,需要第一时间通知外卖商家

  • 语音播报
  • 弹出提示框
设计:
  • 通过WebSocket实现管理端页面和服务端保持长连接状态
  • 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
  • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务器发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
    • type 为消息类型,1为来单提醒 2为客户催单
    • orderId 为订单id
    • content 为消息内容
sky-server  com/sky/service/impl/OrderServiceImpl.java
 /**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 查询订单
        Orders order = orderMapper.getByOrderNumber(ordersPaymentDTO.getOrderNumber());
        if (order == null) {
            throw new OrderBusinessException("订单不存在");
        }

        // 检查订单支付状态
        if (order.getPayStatus() == 1) { // 1 表示已支付
            throw new OrderBusinessException("该订单已支付");
        }
        order.setPayStatus(1);

        // 更新订单支付状态为已支付
        order.setPayStatus(Orders.PAID);
        order.setCheckoutTime(LocalDateTime.now());
        order.setPayMethod(ordersPaymentDTO.getPayMethod());
        order.setStatus(Orders.TO_BE_CONFIRMED);

        // 支付成功后通过 WebSocket 向客户端推送消息
        Map<String, Object> map = new HashMap<>();
        map.put("type", 1); // 1 表示来单提醒
        map.put("orderId", order.getId());
        map.put("content", "订单号:" + ordersPaymentDTO.getOrderNumber());

        webSocketServer.sendToAllClient(JSON.toJSONString(map));


        orderMapper.update(order);

        // 构造并返回支付结果对象
        OrderPaymentVO orderPaymentVO = new OrderPaymentVO();
        orderPaymentVO.setOrderNumber(order.getNumber()); // 订单号
        orderPaymentVO.setPaymentTime(new Date());
        orderPaymentVO.setPaymentStatus("SUCCESS");

        return orderPaymentVO;

    }

用户催单

com/sky/controller/user/OrderController.java 
/**
     * 客户催单
     * @param id
     * @return
     */
    @GetMapping("/reminder/{id}")
    @ApiOperation("客户催单")
    public Result reminder(@PathVariable("id") Long id){
        orderService.reminder(id);
        return Result.success();
    }
com/sky/service/OrderService.java
 /**
     * 用户催单
     * @param id
     */
    void reminder(Long id);
com/sky/service/impl/OrderServiceImpl.java
/**
     * 客户催单
     * @param id
     */
    public void reminder(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在
        if (ordersDB == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Map map = new HashMap();
        map.put("type",2); //1表示来单提醒 2表示客户催单
        map.put("orderId",id);
        map.put("content","订单号:" + ordersDB.getNumber());

        //通过websocket向客户端浏览器推送消息
        webSocketServer.sendToAllClient(JSON.toJSONString(map));
    }

ApacheECharts

http://echarts.apache.org/zh/index.html

  • 柱形图 bar
  • 饼形图
  • 折线图

使用Echarts,重点在于研究当前图标所需数据格式,通常是需要后端提供符合格式要求的动态数据,然后相应给前端来展示图表

营业额统计

业务规则
  • 营业额指订单状态为已完成的订单金额合计
  • 基于可视化报表的折线图展示营业额数据,x轴为日期,y轴为营业额
  • 根据时间选择区间,展示每天的营业额数据
根据接口定义设计对应的vo:
sky-pojo  com/sky/vo/TurnoverReportVO.java
package com.sky.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

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

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03 [开始到结束的每一天]
    private String dateList;

    //营业额,以逗号分隔,例如:406.0,1520.0,75.0 [营业额一一对应]
    private String turnoverList;
}
sky-server  com/sky/controller/admin/ReportController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;

/**
 * 数据统计相关接口
 */
@RestController
@RequestMapping("/admin/report")
@Api(tags = "数据统计接口")
@Slf4j
public class ReportController {
    @Autowired
    private ReportService reportService;

    /**
     * 统计指定时间区间内的营业额数据
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/turnoverStatistics")
    public Result<TurnoverReportVO> turnoverStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate end) {
        log.info("营业额统计:{},{}",begin,end);
        return Result.success(reportService.getTurnoverStatistics(begin,end));
    }
}
sky-server  com/sky/service/ReportService.java
package com.sky.service;

import com.sky.vo.TurnoverReportVO;

import java.time.LocalDate;

public interface ReportService {
    /**
     * 统计指定时间区间内的营业额数据
     * @param begin
     * @param end
     * @return
     */
    TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end);
}
sky-server  com/sky/service/impl/ReportServiceImpl.java
package com.sky.service.impl;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    @Autowired
    private OrderMapper orderMapper;
    /**
     * 统计指定时间区间内的营业额数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
        // 当前集合用于存放从begin到end范围内的每天的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);

        while (!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        // 存放每天的营业额
        List<Double> turnoverList = new ArrayList<>();
        for (LocalDate date : dateList) { //LocalDate只是年月日 而下单的Order有时分秒
            // 查询Date日期对应的营业额数据,数据额是指:订单状态为“已完成”的订单金额合计
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
        // select sum(count) from orders where order_time > ? and order_time < ? and status = 5
            Map map = new HashMap<>();
            map.put("begin", beginTime);
            map.put("end", endTime);
            map.put("status", Orders.COMPLETED);
            Double turnover = orderMapper.sumByMap(map);
            turnover = turnover == null ? 0.0 : turnover;//没有营业额则默认为0
            turnoverList.add(turnover);
        }

        return TurnoverReportVO.builder()
                .dateList(StringUtils.join(dateList, ","))
                .turnoverList(StringUtils.join(turnoverList, ","))
                .build();
    }
}
sky-server  com/sky/mapper/OrderMapper.java
/**
     * 根据动态条件统计营业额数据
     * @param map
     * @return
     */
    Double sumByMap(Map map);
sky-server  mapper/OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">

    <insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
        insert into orders
        (number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
         phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
         tableware_status)
        values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
                #{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
                #{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
    </insert>

    <update id="update" parameterType="com.sky.entity.Orders">
        update orders
        <set>
            <if test="cancelReason != null and cancelReason!='' ">
                cancel_reason=#{cancelReason},
            </if>
            <if test="rejectionReason != null and rejectionReason!='' ">
                rejection_reason=#{rejectionReason},
            </if>
            <if test="cancelTime != null">
                cancel_time=#{cancelTime},
            </if>
            <if test="payStatus != null">
                pay_status=#{payStatus},
            </if>
            <if test="payMethod != null">
                pay_method=#{payMethod},
            </if>
            <if test="checkoutTime != null">
                checkout_time=#{checkoutTime},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="deliveryTime != null">
                delivery_time = #{deliveryTime}
            </if>
        </set>
        where id = #{id}
    </update>

    <select id="pageQuery" resultType="Orders">
        select * from orders
        <where>
            <if test="number != null and number!=''">
                and number like concat('%',#{number},'%')
            </if>
            <if test="phone != null and phone!=''">
                and phone like concat('%',#{phone},'%')
            </if>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="beginTime != null">
                and order_time &gt;= #{beginTime}
            </if>
            <if test="endTime != null">
                and order_time &lt;= #{endTime}
            </if>
        </where>
        order by order_time desc
    </select>
    <!-- 根据订单号查询订单 -->
    <select id="getByOrderNumber" parameterType="String" resultType="Orders">
        select * from orders where number = #{orderNumber}
    </select>
    <select id="sumByMap" resultType="java.lang.Double">
        select sum(amount) from orders
        <where>
            <if test="begin != null">
                and order_time &gt; #{begin}
            </if>
            <if test="end != null">
                and order_time &lt; #{end}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
    </select>
</mapper>

用户统计

业务规则:

  • 根据时间选择区间,展示每天的用户总量和新增用户量数据
据接口定义设计对应的vo:
sky-pojo  com/sky/vo/UserReportVO.java
package com.sky.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

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

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
    private String dateList;

    //用户总量,以逗号分隔,例如:200,210,220
    private String totalUserList;

    //新增用户,以逗号分隔,例如:20,21,10
    private String newUserList;

}
sky-server  com/sky/controller/admin/ReportController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;

/**
     * 用户统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/userStatistics")
    @ApiOperation("用户统计")
    public Result<UserReportVO> userStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate end){
        log.info("用户数据统计:{},{}",begin,end);
        return Result.success(reportService.getUserStatistics(begin,end));
    }
sky-server  com/sky/service/ReportService.java
package com.sky.service;

import com.sky.vo.TurnoverReportVO;

import java.time.LocalDate;

public interface ReportService {
   /**
     * 用户统计
     * @param begin
     * @param end
     * @return
     */
    UserReportVO getUserStatistics(LocalDate begin, LocalDate end);
}
sky-server  com/sky/service/impl/ReportServiceImpl.java
package com.sky.service.impl;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    /**
     * 统计指定时间区间内的用户数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        // 存放从begin 到 end之间的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);
        while (!begin.equals(end)) {
            //日期计算,计算指定日期的后一天的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        // 存放每天新增用户数量 select count(id) from user where create_time > ? and create_time < ?
        List<Integer> newUserList = new ArrayList<>();
        // 存放每天的总用户数量 select count(id) from user where create_time <= ?
        List<Integer> totalUserList = new ArrayList<>();

        for (LocalDate date : dateList) {
        // 遍历每一天的用户总量和数量
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);

            Map map = new HashMap<>();
            map.put("end", endTime);

            // 总用户数量
            Integer integer = userMapper.countByMap(map);

            map.put("begin", beginTime);
            //新增用户数量
            Integer newUser = userMapper.countByMap(map);
            totalUserList.add(integer);
            newUserList.add(newUser);
        }
        return UserReportVO.builder()
                .dateList(StringUtils.join(dateList, ","))
                .totalUserList(StringUtils.join(totalUserList, ","))
                .newUserList(StringUtils.join(newUserList, ","))
                .build();
    }
}
sky-server  com/sky/mapper/UserMapper.java
/**
     * 根据动态条件统计用户数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
sky-server  mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">

<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from orders
        <where>
            <if test="begin != null">
                and order_time &gt; #{begin}
            </if>
            <if test="end != null">
                and order_time &lt; #{end}
            </if>
        </where>
    </select>
</mapper>

订单统计

业务规则
  • 根据时间选择区间,展示每天的订单总数和有效订单数
  • 展示所选时间区间内的有效订单数、总订单数、订单完成率
  • 订单完成率 = 有效订单数 / 总订单数 * 100%
返回数据
  • dataList 日期列表以逗号分隔
  • orderCompletionRate 订单完成率
  • orderCountList 订单数列表以逗号分隔
  • totalOrderCount 订单总数
  • validOrderCount 有效订单数
  • validOrderCountList 有效订单数列表以逗号分隔
据接口定义设计对应的vo:
sky-pojo  com/sky/vo/OrderReportVO.java
package com.sky.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

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

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
    private String dateList;

    //每日订单数,以逗号分隔,例如:260,210,215
    private String orderCountList;

    //每日有效订单数,以逗号分隔,例如:20,21,10
    private String validOrderCountList;

    //订单总数
    private Integer totalOrderCount;

    //有效订单数
    private Integer validOrderCount;

    //订单完成率
    private Double orderCompletionRate;

}
sky-server  com/sky/controller/admin/ReportController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;

/**
     * 订单统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/ordersStatistics")
    @ApiOperation("订单统计")
    public Result<OrderReportVO> ordersStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd")  LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
        log.info("订单数据统计:{},{}",begin,end);
        return Result.success(reportService.getOrderStatistics(begin,end));
    }
sky-server  com/sky/service/ReportService.java
package com.sky.service;

import com.sky.vo.TurnoverReportVO;

import java.time.LocalDate;

public interface ReportService {
 /**
     * 统计指定时间区间内的订单数据
     * @param begin
     * @param end
     * @return
     */
    OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);
}
sky-server  com/sky/service/impl/ReportServiceImpl.java
package com.sky.service.impl;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

 /**
     * 统计指定时间区间内的订单数据
     * @param begin
     * @param end
     * @return
     */
    public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
        //存放从begin到end之间的每天对应的日期
        List<LocalDate> dateList = new ArrayList<>();

        dateList.add(begin);

        while (!begin.equals(end)) {
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        //存放每天的订单总数
        List<Integer> orderCountList = new ArrayList<>();
        //存放每天的有效订单数
        List<Integer> validOrderCountList = new ArrayList<>();

        //遍历dateList集合,查询每天的有效订单数和订单总数
        for (LocalDate date : dateList) {
            //查询每天的订单总数 select count(id) from orders where order_time > ? and order_time < ?
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
            Integer orderCount = getOrderCount(beginTime, endTime, null);

            //查询每天的有效订单数 select count(id) from orders where order_time > ? and order_time < ? and status = 5
            Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);

            orderCountList.add(orderCount);
            validOrderCountList.add(validOrderCount);
        }

        //计算时间区间内的订单总数量
        Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();

        //计算时间区间内的有效订单数量
        Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();

        Double orderCompletionRate = 0.0;
        if(totalOrderCount != 0){
            //计算订单完成率
            orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
        }

        return  OrderReportVO.builder()
                .dateList(StringUtils.join(dateList,","))
                .orderCountList(StringUtils.join(orderCountList,","))
                .validOrderCountList(StringUtils.join(validOrderCountList,","))
                .totalOrderCount(totalOrderCount)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .build();
    }
    /**
     * 根据条件统计订单数量
     * @param begin
     * @param end
     * @param status
     * @return
     */
    private Integer getOrderCount(LocalDateTime begin, LocalDateTime end, Integer status){
        Map map = new HashMap();
        map.put("begin",begin);
        map.put("end",end);
        map.put("status",status);

        return orderMapper.countByMap(map);
    }
sky-server  com/sky/mapper/OrderMapper.java
/**
     * 根据动态条件统计用户数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
sky-server  mapper/OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">

<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from orders
        <where>
            <if test="begin != null">
                and order_time &gt; #{begin}
            </if>
            <if test="end != null">
                and order_time &lt; #{end}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
    </select>
</mapper>

销量排名Top10

产品原型 (查已完成的数据)
  • 此处的销量为商品销售的份数
sky-server  com/sky/controller/admin/ReportController.java
/**
     * 销量排名统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/top10")
    @ApiOperation("销量排名top10")
    public Result<SalesTop10ReportVO> top10(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
        log.info("销量排名top10:{},{}",begin,end);
        return Result.success(reportService.getSalesTop10(begin,end));
    }
sky-server  com/sky/service/ReportService.java
/**
     * 销量排名统计
     * @param begin
     * @param end
     * @return
     */
    SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end);
sky-server  com/sky/service/impl/ReportServiceImpl.java
    /**
     * 统计指定时间区间内的销量排名前10
     * @param begin
     * @param end
     * @return
     */
    @Override
    public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
        LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);

        List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
        List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
        String nameList = StringUtils.join(names, ",");

        List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
        String numberList = StringUtils.join(numbers, ",");

        //封装返回结果数据
        return SalesTop10ReportVO
                .builder()
                .nameList(nameList)
                .numberList(numberList)
                .build();
    }
sky-server  com/sky/mapper/OrderMapper.java
  /**
     * 统计指定时间内的销量排名
     * @return
     */
    List<GoodsSalesDTO> getSalesTop10(LocalDateTime begin,LocalDateTime end);
sky-server  mapper/OrderMapper.xml
<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
        select od.name, sum(od.number) number
        from order_detail od,orders o
        where od.order_id = o.id and o.status = 5
        <if test="begin != null">
            and o.order_time &gt; #{begin}
        </if>
        <if test="end != null">
            and o.order_time &lt; #{end}
        </if>
        group by od.name
        order by number desc
        limit 0,10
    </select>
重装数据库
C:\Windows\System32>cd D:\MySQL\MySQL Server 8.0\bin

C:\Windows\System32>mysqld --install MySQL80
Service successfully installed.

C:\Windows\System32>sc query | findstr MySQL

C:\Windows\System32>net start MySQL80
MySQL80 服务正在启动 .
MySQL80 服务已经启动成功。
服务里的MySQL80是Mysql服务
-----------------------------------------------------------------------------------------

C:\Windows\System32>cd D:\MariaDB 11.0\bin

C:\Windows\System32>mysqld --install MariaDB
Service successfully installed.

C:\Windows\System32>net start MariaDB
MariaDB 服务正在启动 .
MariaDB 服务无法启动。
服务里的MariaDB是MariaDB服务

工作台

工作台是系统运营的数据看板,并提供快捷操作入口,可以有效提高商家的工作效率

功能工作台展示的数据:

  • 今日数据
  • 订单管理
  • 菜品总览
  • 套餐总览
  • 订单信息
名词解释:
  • 营业额:已完成订单的总金额
  • 有效订单:已完成订单的数量
  • 订单完成率:有效订单数 / 总订单数 * 100%
  • 平均客单价:营业额 / 有效订单数
  • 新增用户:新增用户的数量
接口设计:
  • 今日数据接口

    Path: /admin/workspace/businessData
    Method: Get

  • 订单管理接口

    Path: /admin/workspace/overviewOrders
    Method: Get

  • 菜品总览接口

    Path: /admin/workspace/overviewDishes
    Method: Get

  • 套餐总览接口

    Path: /admin/workspace/overviewSetmeals
    Method: Get

  • 订单搜索(已完成)

sky-server  com/sky/controller/admin/WorkSpaceController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.LocalTime;

/**
 * 工作台
 */
@RestController
@RequestMapping("/admin/workspace")
@Slf4j
@Api(tags = "工作台相关接口")
public class WorkSpaceController {

    @Autowired
    private WorkspaceService workspaceService;

    /**
     * 工作台今日数据查询
     * @return
     */
    @GetMapping("/businessData")
    @ApiOperation("工作台今日数据查询")
    public Result<BusinessDataVO> businessData(){
        //获得当天的开始时间
        LocalDateTime begin = LocalDateTime.now().with(LocalTime.MIN);
        //获得当天的结束时间
        LocalDateTime end = LocalDateTime.now().with(LocalTime.MAX);

        BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin, end);
        return Result.success(businessDataVO);
    }

    /**
     * 查询订单管理数据
     * @return
     */
    @GetMapping("/overviewOrders")
    @ApiOperation("查询订单管理数据")
    public Result<OrderOverViewVO> orderOverView(){
        return Result.success(workspaceService.getOrderOverView());
    }

    /**
     * 查询菜品总览
     * @return
     */
    @GetMapping("/overviewDishes")
    @ApiOperation("查询菜品总览")
    public Result<DishOverViewVO> dishOverView(){
        return Result.success(workspaceService.getDishOverView());
    }

    /**
     * 查询套餐总览
     * @return
     */
    @GetMapping("/overviewSetmeals")
    @ApiOperation("查询套餐总览")
    public Result<SetmealOverViewVO> setmealOverView(){
        return Result.success(workspaceService.getSetmealOverView());
    }
}
sky-server  com/sky/service/WorkspaceService.java
package com.sky.service;

import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import java.time.LocalDateTime;

public interface WorkspaceService {

    /**
     * 根据时间段统计营业数据
     * @param begin
     * @param end
     * @return
     */
    BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end);

    /**
     * 查询订单管理数据
     * @return
     */
    OrderOverViewVO getOrderOverView();

    /**
     * 查询菜品总览
     * @return
     */
    DishOverViewVO getDishOverView();

    /**
     * 查询套餐总览
     * @return
     */
    SetmealOverViewVO getSetmealOverView();

}
sky-server  com/sky/service/impl/WorkspaceServiceImpl.java
package com.sky.service.impl;

import com.sky.constant.StatusConstant;
import com.sky.entity.Orders;
import com.sky.mapper.DishMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WorkspaceServiceImpl implements WorkspaceService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 根据时间段统计营业数据
     * @param begin
     * @param end
     * @return
     */
    public BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end) {
        /**
         * 营业额:当日已完成订单的总金额
         * 有效订单:当日已完成订单的数量
         * 订单完成率:有效订单数 / 总订单数
         * 平均客单价:营业额 / 有效订单数
         * 新增用户:当日新增用户的数量
         */

        Map map = new HashMap();
        map.put("begin",begin);
        map.put("end",end);

        //查询总订单数
        Integer totalOrderCount = orderMapper.countByMap(map);

        map.put("status", Orders.COMPLETED);
        //营业额
        Double turnover = orderMapper.sumByMap(map);
        turnover = turnover == null? 0.0 : turnover;

        //有效订单数
        Integer validOrderCount = orderMapper.countByMap(map);

        Double unitPrice = 0.0;

        Double orderCompletionRate = 0.0;
        if(totalOrderCount != 0 && validOrderCount != 0){
            //订单完成率
            orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
            //平均客单价
            unitPrice = turnover / validOrderCount;
        }

        //新增用户数
        Integer newUsers = userMapper.countByMap(map);

        return BusinessDataVO.builder()
                .turnover(turnover)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .unitPrice(unitPrice)
                .newUsers(newUsers)
                .build();
    }


    /**
     * 查询订单管理数据
     *
     * @return
     */
    public OrderOverViewVO getOrderOverView() {
        Map map = new HashMap();
        map.put("begin", LocalDateTime.now().with(LocalTime.MIN));
        map.put("status", Orders.TO_BE_CONFIRMED);

        //待接单
        Integer waitingOrders = orderMapper.countByMap(map);

        //待派送
        map.put("status", Orders.CONFIRMED);
        Integer deliveredOrders = orderMapper.countByMap(map);

        //已完成
        map.put("status", Orders.COMPLETED);
        Integer completedOrders = orderMapper.countByMap(map);

        //已取消
        map.put("status", Orders.CANCELLED);
        Integer cancelledOrders = orderMapper.countByMap(map);

        //全部订单
        map.put("status", null);
        Integer allOrders = orderMapper.countByMap(map);

        return OrderOverViewVO.builder()
                .waitingOrders(waitingOrders)
                .deliveredOrders(deliveredOrders)
                .completedOrders(completedOrders)
                .cancelledOrders(cancelledOrders)
                .allOrders(allOrders)
                .build();
    }

    /**
     * 查询菜品总览
     *
     * @return
     */
    public DishOverViewVO getDishOverView() {
        Map map = new HashMap();
        map.put("status", StatusConstant.ENABLE);
        Integer sold = dishMapper.countByMap(map);

        map.put("status", StatusConstant.DISABLE);
        Integer discontinued = dishMapper.countByMap(map);

        return DishOverViewVO.builder()
                .sold(sold)
                .discontinued(discontinued)
                .build();
    }

    /**
     * 查询套餐总览
     *
     * @return
     */
    public SetmealOverViewVO getSetmealOverView() {
        Map map = new HashMap();
        map.put("status", StatusConstant.ENABLE);
        Integer sold = setmealMapper.countByMap(map);

        map.put("status", StatusConstant.DISABLE);
        Integer discontinued = setmealMapper.countByMap(map);

        return SetmealOverViewVO.builder()
                .sold(sold)
                .discontinued(discontinued)
                .build();
    }
}
sky-server  com/sky/mapper/DishMapper.java
/**
     * 根据条件统计菜品数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
sky-server  mapper/DishMapper.xml
<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from dish
        <where>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
        </where>
    </select>
sky-server  com/sky/mapper/SetmealMapper.java
/**
     * 根据条件统计套餐数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
sky-server  mapper/SetmealMapper.xml
<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from setmeal
        <where>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
        </where>
    </select>

Apache POI

在Java中操控Excel文件 [读写操作]

Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目,POI都是用于操作Excel文件

Apache POI应用场景:
  • 银行网银系统导出交易明细
  • 各种业务系统到出Excel报表
  • 批量导入业务数据
sky-server  com/sky/test/POITest.java
package com.sky.test;

import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;

public class POITest {
    /**
     * 通过POI创建Excel文件并且写入文件内容
     */
    public static void write() throws Exception {
        // 在内存中创建一个Excel文件
        XSSFWorkbook excel = new XSSFWorkbook();
        // 在Excel文件中创建一个sheet页
        XSSFSheet sheet = excel.createSheet("info");
        // 在Sheet中创建行对象, rownum编号从0开始
        XSSFRow row = sheet.createRow(1);
        // 创建单元格并写入文件内容
        row.createCell(1).setCellValue("姓名");
        row.createCell(2).setCellValue("城市");

        // 创建一个新行
        row = sheet.createRow(2);
        row.createCell(1).setCellValue("张三");
        row.createCell(2).setCellValue("北京");

        row = sheet.createRow(3);
        row.createCell(1).setCellValue("李四");
        row.createCell(2).setCellValue("南京");

        // 通过输出流将内存中的Excel文件写入到磁盘
        FileOutputStream out = new FileOutputStream(new File("C:\\Users\\Pluminary\\Desktop\\itcast.xlsx"));
        excel.write(out);

        // 关闭资源
        out.close();
        excel.close();
    }

    public static void main(String[] args) throws Exception {
        write();
    }
}

导出运营数据Excel报表

实现步骤:
  • 设计Excel模板文件
  • 查询近30天的运营数据
  • 将查询到的运营数据写入模板文件
  • 通过输出流将Excel文件下载到客户端浏览器
sky-pojo  com/sky/vo/BusinessDataVO.java
package com.sky.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 数据概览
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BusinessDataVO implements Serializable {

    private Double turnover;//营业额

    private Integer validOrderCount;//有效订单数

    private Double orderCompletionRate;//订单完成率

    private Double unitPrice;//平均客单价

    private Integer newUsers;//新增用户数

}
sky-server  com/sky/controller/admin/ReportController.java
/**
     * 导出运营数据报表
     * @param response
     */
    @GetMapping("/export")
    @ApiOperation("导出运营数据报表")
    public void export(HttpServletResponse response) {
        reportService.exportBusinessData(response);
    }
sky-server  com/sky/service/ReportService.java
 /**
     * 导出运营数据报表
     * @param response
     */
    void exportBusinessData(HttpServletResponse response);
sky-server  com/sky/service/impl/ReportServiceImpl.java
/**
     * 导出运营数据报表
     * @param response
     */
    @Override
    public void exportBusinessData(HttpServletResponse response) {
        // 查询数据库 获取营业数据 -- 查询最近30天的营业数据
        LocalDate dateBegin = LocalDate.now().minusDays(30);
        LocalDate dateEnd = LocalDate.now().minusDays(1);
        // 查询概览数据
        BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN),LocalDateTime.of(dateEnd, LocalTime.MAX));

        // 查询的数据通过POI写入Excel文件中 (获得对象 获得类加载器 类加载器读取资源)
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
        try {
            // 基于模板文件创建一个新的Excel文件
            XSSFWorkbook excel = new XSSFWorkbook();

            // 填充数据 [获取标签页]

            XSSFSheet sheet = excel.getSheet("Sheet1");
            // 获取第二行[索引是从0开始]
            sheet.getRow(1).createCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd);

            // 获得第四行
            XSSFRow row = sheet.getRow(3);
            row.getCell(2).setCellValue(businessDataVO.getTurnover());//营业额
            row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());//订单完成率
            row.getCell(6).setCellValue(businessDataVO.getNewUsers());//新增用户数

            // 获得第五行
            row = sheet.getRow(4);
            row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());//有效订单数
            row.getCell(4).setCellValue(businessDataVO.getUnitPrice());//平均单品价格

            // 填充明细数据
            for (int i = 0; i < 30; i++) {
                LocalDate date = dateBegin.plusDays(i);
                // 查询某一天的营业数据
                workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
                // 获得某一行
                row = sheet.getRow(7 + i);// 利用循环 超越循环
                row.getCell(1).setCellValue(date.toString());
                row.getCell(2).setCellValue(businessDataVO.getTurnover());
                row.getCell(3).setCellValue(businessDataVO.getValidOrderCount());
                row.getCell(6).setCellValue(businessDataVO.getOrderCompletionRate());
                row.getCell(4).setCellValue(businessDataVO.getUnitPrice());
                row.getCell(5).setCellValue(businessDataVO.getNewUsers());
            }

            // 通过输出流将Excel文件下载到客户端浏览器
            ServletOutputStream out = response.getOutputStream();
            excel.write(out);

            // 关闭资源
            out.close();
            excel.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
阅读全文

Vue

2024/7/3

Vue快速入门

简介

MVVM响应式编程模型,避免直接操作DOM,降低DOM操作的复杂性

安装Vscode 安装nodejs
检测是否安装成功 cmd → node -v → npm -v

npm设置镜像[cmd]
C:\Users\Pluminary>npm config set registry https://registry.npmmirror.com
C:\Users\Pluminary>npm config ls
C:\Users\Pluminary>npm config get registry
在Vscode里的终端输入 
PS C:\Users\Pluminary\Desktop\vue2> npm init -y
PS C:\Users\Pluminary\Desktop\vue2> npm install vue

Vue Chrome调试工具 Vue.js devtools 5.3.3 安装包及教程_vuedevtool.crx百度网盘-CSDN博客

简单案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<!--<div>{{name}}</div> 这个就没有被挂载-->
    <div id="app">
        <input type="text" v-model="num"> <!--在对话框里输入的时候改变下面的次数-->
        <button v-on:click="num++">登录</button> <!--实现当点击按钮的时候num数值++-->
        <button v-on:click="loginout()">登出</button>
        <h1>{{name}}, 欢迎您,当前登录第{{num}}次,当前登录时间:{{nowDate()}}</h1>
    </div>
    <script src="./node_modules/vue/dist/vue.js"></script>

    <script>
        // 1. new vue实例 每个Vue应用都是通过Vue函数创建一个新的Vue实例开始的
        new Vue({
            el:"#app", // 挂载的模板
            data(){ // 绑定的数据 把data对象中的所有属性 加到相应视图里
                return{
                    name : "图灵学院",
                    num:1
                }
            },
            methods: {
                nowDate(){
                    return new Date().toLocaleDateString()
                },
                loginout(){
                    this.num--; //当调用实例时要加this
                }
            },
        });
        // 双向绑定:数据发生改变 视图也要随之改变;在谷歌浏览器的Vue调试中
        // 指令:简化对Dom的频繁操作
        // 方法:声明方法可以实现更复杂的操作,声明methods属性中
    </script>
</body>
</html>

指令 v-text、v-html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div v-text="message"></div>{{message}}
        <!-- {{}}在网络延迟情况下会出现暂时显示的情况 -->
        <div v-html="message"></div>
    </div>
</body>
<script>
    // 1.实例Vue
    var vm = new Vue({
        el:'#app',
        data:{
            message: "<a href = '#'>Hello</a>"
        },
    })
// v-text 用于绑定数据,语法v-text="属性",会直接将值作为文本显示
// v-html 会将值进行编译再显示
</script>
</html>

指令 v-bind

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .red{
            background-color: red;
        } 
        .yellow{
            background-color: yellow;
        }
    </style>
</head>
<body>
    <div id="app">    <!--:style="font-size: {{bigFont}}; 错误写法-->
        <div v-bind:title="title" :class="red" :style="{fontSize:bigFont}">
            鼠标悬停查看信息!
        </div>
        <div :class="{yellow:isyellow}">
            <a :href="href" :style="{fontSize:bigFont}">图灵学院</a>
        </div>
        <div>
            <img :src="src"/>
        </div>
        <div>
            <button v-bind:disabled="disabled">禁用按钮</button>
        </div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: "#app",
        data:{
            red:"red",
            isyellow:true,
            bigFont:"50px",
            title: "您好,本网站可以学到更多的知识",
            href: "https://www.pronhub.com/",
            scr: '../Imooc_Cat.jpg',
            disabled: true
        }
    })

    // 绑定元素属性 -- v-bind
    // 语法:v-bind:元素属性="vue的属性"
    // 简写:v-bind:title 简写成 :title
    /*
       针对样式的特殊用法:
         动态控制class是否添加 :class="{red:isred}"
           语法 {red:isred} : {需要动态控制的class样式:vue的属性(需要是boolean类型)}

         动态设置style的样式   :style="{fontSize:bigFont}"
           语法 {fontSize: bigFont} : {css样式的驼峰命名方法:} font-size=fontSize
    */
</script>
</html>

指令 v-model

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div>
            <label>年龄:</label>
            <input v-model="age"/>
        </div>
        <div>当前输入的年龄是:{{age}}</div>
        <button @click="add">加一岁</button> <!--  @click = v-on:click  -->
        <button @click="alertYear">弹出年龄</button>

    <hr>
        <label>课程:</label>
        <select v-model="course">
            <option value="javascript">javascript</option>
            <option value="java">java</option>
            <option value="c++">c++</option>
        </select>

        <div>
            <input v-model="course" type="radio" name="course" value="javascript">javascript
            <input v-model="course" type="radio" name="course" value="java">java
            <input v-model="course" type="radio" name="course" value="c++">c++
        </div>

        <div>当前课程是:{{course}}</div>
    </div>
    
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
    el:"#app",
    data:{
        age:10,
        course:'java'
    },
    methods: {
        add(){
            this.age++;
        },
        alertYear(){
            alert(this.age)
        }
    }
})

//v-model 用于实现双向绑定 一般用在表单元素
</script>
</html>

指令 v-on

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <button v-on:click="hello('hello')">hello</button>
        <button @click="hello('world')">world</button>
        <button @click="num++">数据:</button>{{num}}
<!-- 不让事件传播 禁止默认a标签事件 
 @click.stop 不让事件传播
 @click.self 需要自己点击[不会参与传播]
 @click.prevent 组织默认事件
 @click.once 事件只会触发一次
 -->
        <div @click="hello(1)" style="background-color: red; width: 800px; height: 500px;padding: 50px;">
            1
            <div @click.stop="hello(2)" style="background-color: blue; width: 400px; height: 200px;padding: 50px;">
                2
                <a @click.stop.prevent="hello(3)" style="background-color: yellow; width: 200px; height: 100px; display: block;padding: 50px;" href="www.baidu.com">3</a>
            </div>
        </div>

        <input v-on:keyup.enter="hello('enter')"> <!--键盘按回车触发--><br>
        <input v-on:keyup.space="hello('enter')">
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: "#app",
        data:{
            num: 0
        },
        methods: {
            hello(str){
                alert(str)
            }
        }
    })

/*
 v-on用于绑定事件
 语法  v-on:事件名
 简写方式 v-on:click => @click
 只能调用vue中的函数和数据
*/
</script>
</html>

指令 v-for

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        循环数组
        <ul>
            <li v-for="(item, index) in music">{{item.name}}---{{index+1}}</li>
        </ul>
        循环对象
        <ul>
            <li v-for="(item, index) in obj">{{item}}</li>
        </ul>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
    el: '#app',
    data:{
        // 要循环的数组
        music:[
            {name: '青花瓷'},
            {name: '阳光总在风雨后'},
            {name: '十年'}
        ],
        // 要循环的对象
        obj:{
            name: '句号',
            age: 18,
            sex: '男'
        }
    }
})
/*
v-for循环,可以循环数组和对象
v-for="(item, index) in intems
语法:v-for="(每次循环接收的变量, 当前循环的索引) in 需要循环的变量"
*/
</script>
</html>

指令 v-if 与 v-show

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <button @click="vif=!vif">切换显示隐藏</button>
        <div v-if="vif" style="background-color: red; width: 200px; height: 200px;">
            v-if
        </div>
        <hr>
        <button @click="vshow=!vshow">切换显示隐藏</button>
        <div v-show="vshow" style="background-color: yellow; width: 200px; height: 200px;">
            v-show
        </div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
    el: "#app",
    data:{
        vif: true,
        vshow: true
    }
})
/*
    v-if 和 v-show 用于控制元素显示隐藏
    语法 v-if = "指定Boolean值" v-show一样
    
    v-if 控制元素是否生成
    v-show 控制元素是否显示隐藏
*/
</script>
</html>

指令 v-else 与 v-else-if

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div v-if="number == 1">
            A
        </div>
        <div v-else-if="number == 2">
            B
        </div>
        <div v-else>
            C
        </div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
    el: '#app',
    data() {
        return {
            number: 1
        }
    },
})
</script>
</html>

计算属性和侦听器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>计算属性和侦听器</title>
</head>
<body>
    <div id="app">
        <!-- 实现一个购物车功能 
            1. 数据初始化处理
            2. 选择商品数量:当商品数量超过库存做提示
            3. 计算商品总价
        -->
        <ul>
            <li v-for="(item, index) in car">
                {{item.pname}} --- 商品价格:{{item.price}} --- 库存:{{item.stock}}
                数量:<input type="number" v-model="item.num" style="width: 30px;"/>
            </li>
            <li><strong>总价:</strong>{{calcSum}}</li>
            <!-- <li v-html="warn()"></li> -->
            <li v-html="message"></li>
        </ul>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
 new Vue({
    el:"#app",
    data:{
        car:[
            {pname:'IPhone 12',price:10000,stock:10,num:1},
            {pname:'Mate40 pro',price:7000,stock:3,num:1},
        ],
        message:"",
    },
    // methods:当方法中的数据发生改变,方法会自动调用 所以当数量增加时会自动计算总价
    methods: {
        // calcSum(){
        //     let sum=0;
        //     this.car.forEach(item => {
        //         sum+=item.price*item.num;
        //     });
        //     return sum;
        // }

        // warn(){
        //     let message="";
        //     this.car.forEach(item => {
        //         if(item.num>item.stock){
        //             message+=`${item.pname}的库存超出限制<br/>`
        //         }
        //     });
        //     return message;
        // }
    },
    // 计算属性 上面就不加大括号  <li><strong>总价:</strong>{{calcSum}}</li>
    computed:{
        calcSum(){
            let sum=0;
            this.car.forEach(item => {
                sum+=item.price*item.num;
            });
            return sum;
        }
    },
    // 侦听器 专门用于侦听某些数据的变化,当数据发生变化会自动调节方法 不能像方法那样调用
    watch:{
        // 要侦听的数据 
        car:{
            handler(newvalue,oldvalue){
                this.message="";
                this.car.forEach(item => {
                    if(item.num>item.stock){
                        this.message+=`${item.pname}的库存超出限制<br/>`
                    }
                });
            },
            deep:true
        },
        //侦听message 基础类型的侦听 将侦听数据作为函数就可以了
        message(newvalue,oldvalue){
            console.info(newvalue,oldvalue)
        }
    }
 });
 /*
    方法和计算属性的区别:
    声明方式不一样,调用不一样方法要用"()", 计算属性调用不要加"()"
 */
/*
    方法和侦听器的区别:
        方法:方法中任意数据发生改变了就会自动调用方法 
            可以调用,进行返回值

        侦听器:需要指定具体的侦听数据,只有被具体指定的侦听数据发生了改变才会触发
            不能像方法那样去调用,而是靠vue自动触发 如果初始情况就数量就大于库存是不改变的
*/
</script>
</html>

过滤器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>过滤器</title>
</head>
<body>
    

    <div id="app">
        <ul>
            <li v-for="user in userList">
                姓名:{{user.name}}; 性别:{{user.gender==1?"男":"女"}}
                {{user.gender | filterGender}}
            </li>
        </ul>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script type="text/javascript">
    let vm = new Vue({
        el: "#app",
        data:{
            userList:[
                {id:1, name:'xushu', gender:1},
                {id:2, name:'zhuge', gender:0}
            ]
        },
        //针对数据过滤的
        methods:{
            formateGender(gender){
                if(gender==1){
                    return "~男"
                }else{
                    return "~女"
                }
            }
        },
        filters:{
            filterGender(gender){
                if(gender==1){
                    return "!男"
                }else{
                    return "!女"
                }
            }
        }
    })
</script>
</body>
</html>

Vue-组件化

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>组件化</title>
</head>
<body>
    <div id="app">
        <!-- <div @click='num++'>{{num}}</div> -->
        <!-- <xushu></xushu> -->
        <xushu :num="numxx"></xushu> <!--传递属性-->  <!--输出50-->
        <app-xushu></app-xushu> <!--输出66-->
    </div>

    <div id="app2">
        <!-- <div @click='num++'>{{num}}</div> -->
        <!-- <xushu></xushu> -->
        <xushu :num="numxx"></xushu> <!--传递属性--> <!--输出100-->
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
        // 1.全局组件 在所有的Vue实例中使用
        /*
            1.1 命名:不要使用驼峰命名法 可以用中划线命名
            1.2 模板:必须的
            1.3 数据:一定要函数的方式声明
        */
        Vue.component("xushu", {
            template: `<div @click='num++'>{{num}}</div>`,
            //1.在自己的字方法中声明  
            // data() {
            //     return {
            //         num:10
            //     }
            // },

            //2.在下方父方法中声明并传递过来
            props: ["num"]
        });

        // 2.局部组件 //自己定义下面引用
        const appXushu = {
            template: `<div @click='num++'>{{num}}</div>`,
            data() {
                return {
                    num: 66
                }
            }
        };

        new Vue({
            el: "#app",
            data: {
                numxx: 50
            },
            components: {
                "app-xushu": appXushu
            }
        });

        new Vue({
            el: "#app2",
            data: {
                numxx: 100
            }
        });
    </script>
</body>
</html>

生命周期和钩子函数

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <span id="name">{{name}}</span>
        <button @click="updateName">更新</button>
        <button @click="destroyInstance">销毁</button>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script type="text/javascript">
    var vm = new Vue({
        el: '#app',
        data:{
            name: 'hello !'
        },
        methods: {
            updateName(){
                console.log('准备改名字啦!')
                this.name = 'hello 图灵!'
            },
            destroyInstance(){
                console.log('销毁实例')
                vm.$destroy()
            }
        }
    })
</script>
</html>

vue-router

npm install vue-router@3
npm install vue-router@3 --legacy-peer-deps

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<!-- 1.安装vue-router路由
     2.引入vue-router文件
     3.根据不同url连接到不同的页面,需要使用模板实现,将模板绑定对应的路由地址
-->
<body>
    <div id="app">
        <nav>
            <router-link to="/">Home</router-link>
            <router-link to="/about">About</router-link>
        </nav>
        <router-view></router-view>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.1/dist/vue-router.js"></script>
    <script>
                // 定义组件:原本应该是在外面定义HomeComponent.vue [创建一些组件]
        const HomeComponent = {
            template: `<div><h1>Home</h1><p>Welcome to the Home page!</p></div>`
        };
              // 定义组件:原本应该是在外面定义AboutComponent.vue [创建一些组件]
        const AboutComponent = {
            template: `<div><h1>About</h1><p>Learn more about us on this page.</p></div>`
        };

        // 定义路由:原本应该是外面定义router.js文件来定义路由。
        const routes = [
            { path: '/', component: HomeComponent },
            { path: '/about', component: AboutComponent }
        ];

        // 创建路由实例
        const router = new VueRouter({
            routes
        });

        // 创建并挂载根实例
        const app = new Vue({
            router// 挂载路由
        }).$mount('#app');
    </script>
</body>
</html>

axios

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue with Axios Example</title>
</head>
<body>
    <div id="app">
        <h1>Data from API</h1>
        <ul>
            <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
        <!-- 使用 Vue 的 v-for 指令遍历 posts 数组,并生成列表项,每个列表项显示 post.title。 -->
        </ul>
    </div>

    <!-- 引入 Vue.js -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <!-- 引入 Axios -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
        // 创建 Vue 实例
        let vm = new Vue({
            el: "#app", // Vue 实例绑定到 HTML 中的 id="app" 的元素
            data: {
                posts: [] // 初始化一个空数组用于存储从 API 获取的数据
            },
            created() {
                // Vue 实例创建时调用的生命周期钩子
                axios.get('https://jsonplaceholder.typicode.com/posts') // 使用 Axios 发送 GET 请求
                    .then(response => { // 请求成功后的处理
                        this.posts = response.data; // 将响应数据赋值给 posts 数组
                    })
                    .catch(error => { // 请求失败后的处理
                        console.error('Error fetching data:', error); // 输出错误信息到控制台
                    });
            }
        });
    </script>
</body>
</html>

安装脚手架

CLI文档,开箱即用!
cmd全局安装:
npm install -g @vue/cli         或者
npm install -g @vue/cli-init

去想要的文件夹里面:C:\Users\Pluminary\Desktop\vue_cli>vue init webpack tuling
vue init webpack 项目名

? Project name tuling
? Project description A Vue.js project
? Author pcy
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No //严格检验Javascript语法的
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm

完成后需要开启:
cd tuling
npm run dev

DONE  Compiled successfully in 1780ms 18:41:21
I  Your application is running here: http://localhost:8080

运用element-ui创建一个简易的登录界面

<template>
  <div class="hello-world">
    <h1>{{ message }}</h1>
    <el-button type="primary" @click="showAlert">Click Me</el-button>
  </div>

  <div class="app">
    <h3>{{passage}}</h3>
    <h4>{{getVal()}}</h4>
  </div>

   <!-- 使用Element UI的Table组件展示empList -->
  <el-table :data="empList" style="width: 100%">
    <el-table-column prop="name" label="Name" width="180"></el-table-column>
    <el-table-column prop="salary" label="Salary" width="180"></el-table-column>
  </el-table>

   <!-- 登录表单 -->
  <el-form ref="loginForm" :model="loginForm" class="loginForm" label-width="80px">
    <h2 class="loginTitle">人事后台管理系统</h2>
    <el-form-item label="用户名" prop="username">
      <el-input v-model="loginForm.username" placeholder="请输入用户名"></el-input>
    </el-form-item>
    <el-form-item label="密码" prop="password">
      <el-input v-model="loginForm.password" type="password" placeholder="请输入密码"></el-input>
    </el-form-item>
    <el-form-item label="验证码" prop="code">
      <el-input v-model="loginForm.code" placeholder="请输入验证码"></el-input>
    </el-form-item>
    <el-form-item style="width:100%;">
      <el-button type="primary" @click="handleLogin">登录</el-button>
    </el-form-item>
  </el-form>
   

</template>

<script>
  export default {
    name: 'app',
    data(){
      return{
        passage: 'Oh?',
        message: 'Hello World!',
        empList:[
          { name: 'Peter', salary: '20000' },
          { name: 'Mike', salary: '16000' },
          { name: 'Tom', salary: '17000' }
        ],
        loginForm:{
          username:'',
          password:'',
          code:''
        }
      }
    },
    methods: {
      showAlert() {
        this.$message({
          message: 'Hello from Element UI!',
          type: 'success'
        });
      },
      getVal:function(){
        return "getVal的方法";
      },
      handleLogin() {
      this.$message({
        message: `登录成功,用户名: ${this.loginForm.username}`,
        type: 'success'
      });
     }
    }
  }

</script>

<style scoped>
    .hello-world {
      text-align: center;
      margin-top: 20px;
    }

    h1 {
      font-size: 24px;
      margin-bottom: 20px;
    }
    .app {
      margin-top: 30px;
    }
</style>
在Vue项目中,Element UI需要正确引入CSS样式和JavaScript文件才能正常工作。如果你没有一个HTML模板文件(例如index.html),而是在Vue组件中直接编写代码,你需要通过一些方式来确保Element UI的样式和脚本被正确加载。

// 1. 创建index.html文件:
这个index.html文件通常放在public文件夹下,是你Vue应用的入口文件。Vue CLI自动生成的项目通常会有这个文件。

// 2. 为什么需要这个HTML文件?
这个index.html文件是Vue CLI或手动设置的Vue项目的入口页面。当你运行npm run serve时,Vue CLI会将所有内容注入到<div id="app"></div>中,这个HTML文件是项目的基本框架。

Element UI的CSS和JS资源:需要在HTML文件的<head>中引入Element UI的样式文件(CSS)和在<body>中引入JS库,这样才能确保Element UI组件在你的Vue组件中正确渲染和运行。
    
// 3. 直接在Vue组件中引入:
如果你不想使用外部HTML文件,Vue项目中可以直接在main.js中引入Element UI的样式和组件:
/*
    // main.js
    import Vue from 'vue';
    import App from './App.vue';
    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';

    Vue.config.productionTip = false;

    Vue.use(ElementUI);

    new Vue({
      render: h => h(App),
    }).$mount('#app');
*/
阅读全文

Cloud分布式微服务打造大型自媒体3大业务平台

2024/5/12

自媒体项目

VM CentOS7:数据库密码→Panchunyao123!
开发思维与企业一致
  • 三端融合:门户+媒体+运营
  • 4g自媒体辉煌时代
  • 5g科技互联网风口
  • 前后端分离式开发、代码动静分离、保证职
  • 能解耦、功能模块互相协调
如何整合分布式中间件到项目中
  • 整个流程会做到细致入微
  • 帮助迅速提升至少2年以上的项目经验
重点功能技术分析
  • Redis:分布式会话、session共享、单点登录、防刷、计数
  • Fastdfs+Nginx/OSS/GridFS:搭建分布式文件系统、单文件/批量上传、人脸隐私保护拦截
  • Maven:项目构建、聚合、分层、架构设计、面向对象
  • 阿里AI:人脸对比、文本/图片自动审核、短信
  • SpringCloud:业务分而治之、可伸缩、可扩展、接口服务化
  • Freemarker:构建模块页,实现页面静态化
前置技能必备
  • Java基础
  • 熟悉MySQL/MariaDB
  • 掌握Linux的基本命令
课程安排
  • 前端构建与运行
  • 后端手把手从0到1
  • 中间件手把手部署

前后端分离开发模式

传统JavaWeb开发 与 前后端页面交互

运行前端项目

Linux 安装 MySQL【CentOS】_linux 安装mysql-CSDN博客

前端代码在压缩包中 启动D:\apache-tomcat-8.5.93\bin\startup.bat
将里面的imooc-news放到D:\apache-tomcat-8.5.93\webapps中
去浏览器中启动 http://localhost:9090/imooc-news/portal/index.html
SwitchHosts

https://oldj.github.io/SwitchHosts/
在本地把域名和对应的IP給联系绑定起来 [相当于在云服务买了域名后绑定]

#imooc-news 127.0.0.1
[这东西需要关闭代理才可以用]

127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com

127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com

D:\apache-tomcat-8.5.93\webapps\imooc-news\portal\js\app.js
window.app = {
    /* 
    portalIndexUrl: "http://localhost:8080/imooc-news/portal/index.html",           // 门户首页地址
    writerIndexUrl: "http://localhost:8080/imooc-news/writer/contentMng.html",      // 作家中心首页
    writerInfoUrl: "http://localhost:8080/imooc-news/writer/accountInfo.html",     // 用户信息完善页面
    userServerUrl: "http://192.168.1.5:8003",   // 用户服务后端接口地址
    */

    portalIndexUrl: "http://www.imoocnews.com:9090/imooc-news/portal/index.html",           // 门户首页地址
    writerLoginUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/passport.html",      // 登录页面
    writerIndexUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/contentMng.html",      // 作家中心首页
    writerInfoUrl: "http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html",     // 用户信息完善页面
    adminCenterUrl: "http://admin.imoocnews.com:9090/imooc-news/admin/contentReview.html",     // 运营管理平台主页

    userServerUrl: "http://user.imoocnews.com:8003",        // 用户服务后端接口地址
    fsServerUrl: "http://files.imoocnews.com:8004",         // 文件服务后端接口地址
    adminServerUrl: "http://admin.imoocnews.com:8005",      // 运营管理服务后端接口地址
    articleServerUrl: "http://article.imoocnews.com:8001",      // 文章服务后端接口地址

    /**
     * 如果本地使用localhost测试可以不使用,如果是ip或者域名测试,cookieDomain改为对应的ip或者域名
     * 例:
     *    ip:  192.168.1.111
     *    域名:   .imooc.com
     */
    cookieDomain: ".imoocnews.com",  
    ......
}

数据库选型与数据导入

  • MySql 5.6/5.7
  • MariaDB
  • Mysql 8.0
表名                             注释
admin_user                      运营管理平台的admin级别用户    
app_user                      网站用户
article                          文章资讯表
category                      新闻资讯文章的分类(或称之为领域)
comments                      评论表
fans                          粉丝表,用户与粉丝的关联关系,粉丝本质也是用户

构建Maven聚合工程

创建一个 imooc-news-dev 的Maven项目作为一个顶级工程项目

pom.xml
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>imooc-news-dev</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
<!--
    1.聚合工程可以分为顶级项目(顶级工程,父工程) 与子工程(子modele模块)
      这两者的关系其实就是父子继承关系, 子工程在maven中可以称为module,
      模块与模块之间是平级的,是可以相互依赖的
    2.子模块可以使用顶级工程中所有的资源(依赖), 子模块之间如果有要使用资源的话
      必须构建依赖(构建关系)
    3.一个顶级工程是可以由多个不同的子工程共同组合而成
-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath /> <!--SpringBoot是和后续的SpringCLoud版本联系的-->
    </parent>

    <properties> <!--属性文件参数 如果mysql是8以上 需要修改mysql的版本号-->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>

        <mysql-connector-java.version>8.0.33</mysql-connector-java.version>
        <mybatis-spring-boot-starter.version>2.1.0</mybatis-spring-boot-starter.version>
        <mapper-spring-boot-starter.version>2.1.5</mapper-spring-boot-starter.version>
        <pagehelper-spring-boot-starter.version>1.2.12</pagehelper-spring-boot-starter.version>

        <okhttp.version>4.2.2</okhttp.version>
        <jackson.version>2.10.2</jackson.version>

        <commons-codec.version>1.11</commons-codec.version>
        <commons-lang3.version>3.4</commons-lang3.version>
        <commons-fileupload.version>1.4</commons-fileupload.version>
        <google-guava.version>28.2-jre</google-guava.version>

        <springfox-swagger2.version>2.4.0</springfox-swagger2.version>
        <swagger-bootstrap-ui.version>1.6</swagger-bootstrap-ui.version>
        <fastdfs.version>1.27.2</fastdfs.version>

        <slf4j.version>1.7.21</slf4j.version>
        <joda-time.version>2.10.6</joda-time.version>
    </properties>

    <!--
        使用dependencyManagement的目的是为了保证父工程的干净,
        也就是说父工程他只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖。
        如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系,
        依赖的导入只有在各自的子工程中才会进行导入。
    -->
<!--  ↓ 管理依赖 不会从外网下载具体jar包 只有在后续子模块配置的时候才会去配置
      为了保证父工程的干净,父工程中只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖
      如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系
      依赖的导入只有在各自的子工程中才会导入
 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringCloud 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 引入 mongodb 依赖 -->
            <dependency>
                <groupId>org.mongodb</groupId>
                <artifactId>mongodb-driver</artifactId>
                <!--mongodb-driver.version-->
                <version>3.11.1</version>
            </dependency>
            <!-- mysql 驱动  这样引用方便以后jar包依赖的升级-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot-starter.version}</version>
            </dependency>
            <!-- 通用mapper逆向工具 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper-spring-boot-starter.version}</version>
            </dependency>
            <!--pagehelper -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper-spring-boot-starter.version}</version>
            </dependency>
            <!--服务和服务之间的请求-->
            <dependency>
                <groupId>com.squareup.okhttp3</groupId>
                <artifactId>okhttp</artifactId>
                <version>${okhttp.version}</version>
            </dependency>

            <!-- jackson -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>

            <!-- apache 工具类 -->
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>${commons-codec.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>${commons-fileupload.version}</version>
            </dependency>

            <!-- google 工具类 -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${google-guava.version}</version>
            </dependency>

            <!-- swagger2 配置 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>swagger-bootstrap-ui</artifactId>
                <version>${swagger-bootstrap-ui.version}</version>
            </dependency>

            <!-- 文件上传fdfs工具 -->
            <dependency>
                <groupId>com.github.tobato</groupId>
                <artifactId>fastdfs-client</artifactId>
                <version>${fastdfs.version}</version>
            </dependency>

            <!-- joda-time 时间工具 -->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>${joda-time.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Java 编译 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
imooc-news-dev-common
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-common:
    通用工程
    包含了一些工具类,枚举类,封装的一些公共方法以及一些第三方组件等
-->
    <artifactId>imooc-news-dev-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>
imooc-news-dev-model
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-model
    模型工程, 所有的子工程以及微服务中所涉及到的模型实体类都在此管理
    可以包含一些 *pojo,*Bean,*Entity,vo,bo,dto等
-->

    <artifactId>imooc-news-dev-model</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
<!--  子工程依赖common -->
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
imooc-news-dev-service-api
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!--    imooc-news-dev-service-api
            接口工程,集中管理所有的controller中的接口,为了更好的统一管理微服务
    -->
    <artifactId>imooc-news-dev-service-api</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
imooc-news-dev-service-user
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-user</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--    引入SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
    </dependencies>
</project>
imooc-news-dev [父]
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>imooc-news-dev</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
    <!-- 这里是子模块[自动创建] 如果物理删除了子工程 这个不会自动删除的要手动删除-->
        <module>imooc-news-dev-common</module>
        <module>imooc-news-dev-model</module>
        <module>imooc-news-dev-service-api</module>
        <module>imooc-news-dev-service-user</module>
        <module>imooc-news-dev-service-user</module>
    </modules>
    <!--
        1.聚合工程可以分为顶级项目(顶级工程,父工程) 与子工程(子modele模块)
          这两者的关系其实就是父子继承关系, 子工程在maven中可以称为module,
          模块与模块之间是平级的,是可以相互依赖的
        2.子模块可以使用顶级工程中所有的资源(依赖), 子模块之间如果有要使用资源的话
          必须构建依赖(构建关系)
        3.一个顶级工程是可以由多个不同的子工程共同组合而成
    -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath /> <!--SpringBoot是和后续的SpringCLoud版本联系的-->
    </parent>

    <properties> <!--属性文件参数 如果mysql是8以上 需要修改mysql的版本号-->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>

        <mysql-connector-java.version>8.0.33</mysql-connector-java.version>
        <mybatis-spring-boot-starter.version>2.1.0</mybatis-spring-boot-starter.version>
        <mapper-spring-boot-starter.version>2.1.5</mapper-spring-boot-starter.version>
        <pagehelper-spring-boot-starter.version>1.2.12</pagehelper-spring-boot-starter.version>

        <okhttp.version>4.2.2</okhttp.version>
        <jackson.version>2.10.2</jackson.version>

        <commons-codec.version>1.11</commons-codec.version>
        <commons-lang3.version>3.4</commons-lang3.version>
        <commons-fileupload.version>1.4</commons-fileupload.version>
        <google-guava.version>28.2-jre</google-guava.version>

        <springfox-swagger2.version>2.4.0</springfox-swagger2.version>
        <swagger-bootstrap-ui.version>1.6</swagger-bootstrap-ui.version>
        <fastdfs.version>1.27.2</fastdfs.version>

        <slf4j.version>1.7.21</slf4j.version>
        <joda-time.version>2.10.6</joda-time.version>
    </properties>

    <!--
        使用dependencyManagement的目的是为了保证父工程的干净,
        也就是说父工程他只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖。
        如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系,
        依赖的导入只有在各自的子工程中才会进行导入。
    -->
<!--  ↓ 管理依赖 不会从外网下载具体jar包 只有在后续子模块配置的时候才会去配置
      为了保证父工程的干净,夫工程中只负责管理依赖,以及依赖的版本,而不会导入额外的jar依赖
      如此一来父工程的职责就很单一了,而且也符合了面向对象开发的父子继承关系
      依赖的导入只有在各自的子工程中才会导入
 -->
    <dependencyManagement>
        <dependencies>
            <!-- SpringCloud 依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- 引入 mongodb 依赖 -->
            <dependency>
                <groupId>org.mongodb</groupId>
                <artifactId>mongodb-driver</artifactId>
                <!--mongodb-driver.version-->
                <version>3.11.1</version>
            </dependency>

            <!-- mysql 驱动  这样引用方便以后jar包依赖的升级-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java.version}</version>
            </dependency>
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot-starter.version}</version>
            </dependency>
            <!-- 通用mapper逆向工具 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper-spring-boot-starter.version}</version>
            </dependency>
            <!--pagehelper -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper-spring-boot-starter.version}</version>
            </dependency>
            <!--服务和服务之间的请求-->
            <dependency>
                <groupId>com.squareup.okhttp3</groupId>
                <artifactId>okhttp</artifactId>
                <version>${okhttp.version}</version>
            </dependency>

            <!-- jackson -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>

            <!-- apache 工具类 -->
            <dependency>
                <groupId>commons-codec</groupId>
                <artifactId>commons-codec</artifactId>
                <version>${commons-codec.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>${commons-fileupload.version}</version>
            </dependency>

            <!-- google 工具类 -->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${google-guava.version}</version>
            </dependency>

            <!-- swagger2 配置 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${springfox-swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>swagger-bootstrap-ui</artifactId>
                <version>${swagger-bootstrap-ui.version}</version>
            </dependency>

            <!-- 文件上传fdfs工具 -->
            <dependency>
                <groupId>com.github.tobato</groupId>
                <artifactId>fastdfs-client</artifactId>
                <version>${fastdfs.version}</version>
            </dependency>

            <!-- joda-time 时间工具 -->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>${joda-time.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Java 编译 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Object hello(){
        return "hello";
    }
}
com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
  port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: service-user
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
SwitchHosts【一定要先关闭代理 因为它会固定静态ip】
127.0.0.1 user.imoocnews.com

http://user.imoocnews.com:8003/hello

api接口暴露

所有业务下不同的controller都要交给api统一接口去处理,把实现所对应的接口写进API内
把imooc-news-dev-service-user中的pom.xml中的关于SpringBoot的依赖
全部放入imooc-news-dev-service-api中
并且在imooc-news-dev-service-user中写入引用依赖
    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
【服务层依赖api层】
更改把user项目的HelloController 复制到dev-service-api 创建一个接口

...api
com/imooc/api/controller/user/HelloControllerApi.java
package com.imooc.api.controller.user;
import org.springframework.web.bind.annotation.GetMapping;

public interface HelloControllerApi {
    /**
     * api的作用:
     * api就相当于企业的领导,老板,部门经理
     * 其他的服务层都是实现,他们就相当于员工,只做事情
     * 老板(开发人员)来看一下每个人(服务)的进度,做什么事
     * 老板不会去问员工,他只会对接部门经理
     * 这里所有的api接口就是统一在这里管理和调度的,微服务也如此
     */

    /**
     * 运作:
     * 现在的所有接口都在此暴露,实现都是在各自的微服务中
     * 本项目只写项目,不写实现,实现在各自的微服务工程中,因为以业务来划分的微服务有很多
     * Controller也会分散在各个微服务工程中,一旦多了就很难统一管理和查看
     *
     * 其次,微服务之间的调用都是基于接口的
     * 如果不这样做,微服务之间的调用就需要互相依赖了
     * 耦合对也就很高,接口的目的是为了能够提供解耦
     *
     * 此外,本项目的接口其实就是一套规范.实现都是由各自的工程去做的处理
     * 目前我们使用springboot作为接口的实现的
     * 如果未来以后出现新的java web框架,那么我们不需要修改接口
     * 只需要去修改对应的实现就可以了,这其实也是解耦的一个体现
     *
     * Swagger2, 基于接口的自动文档生成
     * 所有的配置文件只需要一份,就能再当前项目中去构建了
     * 管理起来很方便
     * 
     * 综上所述,这样做法可以提高多服务的项目可扩展性
     */
    @GetMapping("/hello")
    public Object hello();
}


...user
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    public Object hello(){
        return "hello";
    }
}

配置logback日志与多环境profile

imooc-news-dev-service-user

先添加一个logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-admin"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-dev"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/service-user.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

        return "hello";
    }
}
//重新启动后去页面刷新一下
......
29:22.373 [main] INFO  com.imooc.user.Application - Started Application in 1.061 seconds (JVM running for 1.543)
29:40.693 [http-nio-8003-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
29:40.694 [http-nio-8003-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
29:40.696 [http-nio-8003-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 2 ms
29:40.707 [http-nio-8003-exec-1] INFO  c.i.user.controller.HelloController - info: hello~
29:40.707 [http-nio-8003-exec-1] WARN  c.i.user.controller.HelloController - warn: hello~
29:40.707 [http-nio-8003-exec-1] ERROR c.i.user.controller.HelloController - error: hello~
application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-user
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
application-dev.yml
server:
  port: 8003


application-prod.yml
server:
  port: 8130

优雅的返回封装结果

com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.IMOOCJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

//        return "hello";
//        return IMOOCJSONResult.ok();
//        return IMOOCJSONResult.ok("hello!");
        return IMOOCJSONResult.errorMsg("您的信息有误");
    }
}
com/imooc/grace/result/IMOOCJSONResult.java
package com.imooc.grace.result;

/**
 * 
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 * 
 *                 200:表示成功
 *                 500:表示错误,错误信息在msg字段中
 *                 501:bean验证错误,不管多少个错误都以map形式返回
 *                 502:拦截器拦截到用户token出错
 *                 555:异常抛出信息
 *                 556: 用户qq校验异常
 *                 557: 校验用户是否在CAS登录,用户门票的校验
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V1.0
 * 这样太麻烦了 直接用枚举类
 */
public class IMOOCJSONResult {

    // 响应业务状态
    private Integer status;

    // 响应消息
    private String msg;

    // 响应中的数据
    private Object data;
    
    private String ok;    // 不使用

    public static IMOOCJSONResult build(Integer status, String msg, Object data) {
        return new IMOOCJSONResult(status, msg, data);
    }

    public static IMOOCJSONResult build(Integer status, String msg, Object data, String ok) {
        return new IMOOCJSONResult(status, msg, data, ok);
    }
    
    public static IMOOCJSONResult ok(Object data) {
        return new IMOOCJSONResult(data);
    }

    public static IMOOCJSONResult ok() {
        return new IMOOCJSONResult(null);
    }
    
    public static IMOOCJSONResult errorMsg(String msg) {
        return new IMOOCJSONResult(500, msg, null);
    }

    public static IMOOCJSONResult errorUserTicket(String msg) {
        return new IMOOCJSONResult(557, msg, null);
    }
    
    public static IMOOCJSONResult errorMap(Object data) {
        return new IMOOCJSONResult(501, "error", data);
    }
    
    public static IMOOCJSONResult errorTokenMsg(String msg) {
        return new IMOOCJSONResult(502, msg, null);
    }
    
    public static IMOOCJSONResult errorException(String msg) {
        return new IMOOCJSONResult(555, msg, null);
    }
    
    public static IMOOCJSONResult errorUserQQ(String msg) {
        return new IMOOCJSONResult(556, msg, null);
    }

    public IMOOCJSONResult() {

    }

    public IMOOCJSONResult(Integer status, String msg, Object data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }
    
    public IMOOCJSONResult(Integer status, String msg, Object data, String ok) {
        this.status = status;
        this.msg = msg;
        this.data = data;
        this.ok = ok;
    }

    public IMOOCJSONResult(Object data) {
        this.status = 200;
        this.msg = "OK";
        this.data = data;
    } Getter+Setter
这样太麻烦而且观察起来不方便 升级一下变成枚举类 更加优雅!
com/imooc/user/controller/HelloController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.IMOOCJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        logger.debug("debug: hello~");
        logger.info("info: hello~");
        logger.warn("warn: hello~");
        logger.error("error: hello~");

//        return "hello";
//        return IMOOCJSONResult.ok();
//        return IMOOCJSONResult.ok("hello!");
//        return IMOOCJSONResult.errorMsg("您的信息有误");
        return GraceJSONResult.errorCustom(ResponseStatusEnum.NO_AUTH);
    }
}
com/imooc/grace/result/GraceJSONResult.java
package com.imooc.grace.result;

import java.util.Map;

/**
 * 自定义响应数据类型枚举升级版本
 *
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 *
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V2.0
 */
public class GraceJSONResult {

    // 响应业务状态码
    private Integer status;

    // 响应消息
    private String msg;

    // 是否成功
    private Boolean success;

    // 响应数据,可以是Object,也可以是List或Map等
    private Object data;

    /**
     * 成功返回,带有数据的,直接往OK方法丢data数据即可
     * @param data
     * @return
     */
    public static GraceJSONResult ok(Object data) {
        return new GraceJSONResult(data);
    }
    /**
     * 成功返回,不带有数据的,直接调用ok方法,data无须传入(其实就是null)
     * @return
     */
    public static GraceJSONResult ok() {
        return new GraceJSONResult(ResponseStatusEnum.SUCCESS);
    }
    public GraceJSONResult(Object data) {
        this.status = ResponseStatusEnum.SUCCESS.status();
        this.msg = ResponseStatusEnum.SUCCESS.msg();
        this.success = ResponseStatusEnum.SUCCESS.success();
        this.data = data;
    }


    /**
     * 错误返回,直接调用error方法即可,当然也可以在ResponseStatusEnum中自定义错误后再返回也都可以
     * @return
     */
    public static GraceJSONResult error() {
        return new GraceJSONResult(ResponseStatusEnum.FAILED);
    }

    /**
     * 错误返回,map中包含了多条错误信息,可以用于表单验证,把错误统一的全部返回出去
     * @param map
     * @return
     */
    public static GraceJSONResult errorMap(Map map) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, map);
    }

    /**
     * 错误返回,直接返回错误的消息
     * @param msg
     * @return
     */
    public static GraceJSONResult errorMsg(String msg) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, msg);
    }

    /**
     * 错误返回,token异常,一些通用的可以在这里统一定义
     * @return
     */
    public static GraceJSONResult errorTicket() {
        return new GraceJSONResult(ResponseStatusEnum.TICKET_INVALID);
    }

    /**
     * 自定义错误范围,需要传入一个自定义的枚举,可以到[ResponseStatusEnum.java[中自定义后再传入
     * @param responseStatus
     * @return
     */
    public static GraceJSONResult errorCustom(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }
    public static GraceJSONResult exception(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }

    public GraceJSONResult(ResponseStatusEnum responseStatus) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, Object data) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
        this.data = data;
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, String msg) {
        this.status = responseStatus.status();
        this.msg = msg;
        this.success = responseStatus.success();
    }Getter+Setter
}
com/imooc/grace/result/ResponseStatusEnum.java
package com.imooc.grace.result;

/**
 * 响应结果枚举,用于提供给GraceJSONResult返回给前端的
 * 本枚举类中包含了很多的不同的状态码供使用,可以自定义
 * 便于更优雅的对状态码进行管理,一目了然
 */
public enum ResponseStatusEnum {

    SUCCESS(200, true, "操作成功!"),
    FAILED(500, false, "操作失败!"),

    // 50x
    UN_LOGIN(501,false,"请登录后再继续操作!"),
    TICKET_INVALID(502,false,"会话失效,请重新登录!"),
    NO_AUTH(503,false,"您的权限不足,无法继续操作!"),
    MOBILE_ERROR(504,false,"短信发送失败,请稍后重试!"),
    SMS_NEED_WAIT_ERROR(505,false,"短信发送太快啦~请稍后再试!"),
    SMS_CODE_ERROR(506,false,"验证码过期或不匹配,请稍后再试!"),
    USER_FROZEN(507,false,"用户已被冻结,请联系管理员!"),
    USER_UPDATE_ERROR(508,false,"用户信息更新失败,请联系管理员!"),
    USER_INACTIVE_ERROR(509,false,"请前往[账号设置]修改信息激活后再进行后续操作!"),
    FILE_UPLOAD_NULL_ERROR(510,false,"文件不能为空,请选择一个文件再上传!"),
    FILE_UPLOAD_FAILD(511,false,"文件上传失败!"),
    FILE_FORMATTER_FAILD(512,false,"文件图片格式不支持!"),
    FILE_MAX_SIZE_ERROR(513,false,"仅支持500kb大小以下的图片上传!"),
    FILE_NOT_EXIST_ERROR(514,false,"你所查看的文件不存在!"),
    USER_STATUS_ERROR(515,false,"用户状态参数出错!"),
    USER_NOT_EXIST_ERROR(516,false,"用户不存在!"),

    // 自定义系统级别异常 54x
    SYSTEM_INDEX_OUT_OF_BOUNDS(541, false, "系统错误,数组越界!"),
    SYSTEM_ARITHMETIC_BY_ZERO(542, false, "系统错误,无法除零!"),
    SYSTEM_NULL_POINTER(543, false, "系统错误,空指针!"),
    SYSTEM_NUMBER_FORMAT(544, false, "系统错误,数字转换异常!"),
    SYSTEM_PARSE(545, false, "系统错误,解析异常!"),
    SYSTEM_IO(546, false, "系统错误,IO输入输出异常!"),
    SYSTEM_FILE_NOT_FOUND(547, false, "系统错误,文件未找到!"),
    SYSTEM_CLASS_CAST(548, false, "系统错误,类型强制转换错误!"),
    SYSTEM_PARSER_ERROR(549, false, "系统错误,解析出错!"),
    SYSTEM_DATE_PARSER_ERROR(550, false, "系统错误,日期解析出错!"),

    // admin 管理系统 56x
    ADMIN_USERNAME_NULL_ERROR(561, false, "管理员登录名不能为空!"),
    ADMIN_USERNAME_EXIST_ERROR(562, false, "管理员登录名已存在!"),
    ADMIN_NAME_NULL_ERROR(563, false, "管理员负责人不能为空!"),
    ADMIN_PASSWORD_ERROR(564, false, "密码不能为空后者两次输入不一致!"),
    ADMIN_CREATE_ERROR(565, false, "添加管理员失败!"),
    ADMIN_PASSWORD_NULL_ERROR(566, false, "密码不能为空!"),
    ADMIN_NOT_EXIT_ERROR(567, false, "管理员不存在或密码错误!"),
    ADMIN_FACE_NULL_ERROR(568, false, "人脸信息不能为空!"),
    ADMIN_FACE_LOGIN_ERROR(569, false, "人脸识别失败,请重试!"),
    CATEGORY_EXIST_ERROR(570, false, "文章分类已存在,请换一个分类名!"),

    // 媒体中心 相关错误 58x
    ARTICLE_COVER_NOT_EXIST_ERROR(580, false, "文章封面不存在,请选择一个!"),
    ARTICLE_CATEGORY_NOT_EXIST_ERROR(581, false, "请选择正确的文章领域!"),
    ARTICLE_CREATE_ERROR(582, false, "创建文章失败,请重试或联系管理员!"),
    ARTICLE_QUERY_PARAMS_ERROR(583, false, "文章列表查询参数错误!"),
    ARTICLE_DELETE_ERROR(584, false, "文章删除失败!"),
    ARTICLE_WITHDRAW_ERROR(585, false, "文章撤回失败!"),
    ARTICLE_REVIEW_ERROR(585, false, "文章审核出错!"),
    ARTICLE_ALREADY_READ_ERROR(586, false, "文章重复阅读!"),

    // 人脸识别错误代码
    FACE_VERIFY_TYPE_ERROR(600, false, "人脸比对验证类型不正确!"),
    FACE_VERIFY_LOGIN_ERROR(601, false, "人脸登录失败!"),

    // 系统错误,未预期的错误 555
    SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),
    SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),
    SYSTEM_RESPONSE_NO_INFO(557, false, "");


    // 响应业务状态
    private Integer status;
    // 调用是否成功
    private Boolean success;
    // 响应消息,可以为成功或者失败的消息
    private String msg;

    ResponseStatusEnum(Integer status, Boolean success, String msg) {
        this.status = status;
        this.success = success;
        this.msg = msg;
    }

    public Integer status() {
        return status;
    }
    public Boolean success() {
        return success;
    }
    public String msg() {
        return msg;
    }
}
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": null
}

配置数据库逆向生成实体类

引入mybatis-generator-database[新建工程项目]
pom.xml
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>mybatis-generator-database</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>

        <!-- 引入log4j日志依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>1.3.8.RELEASE</version>
        </dependency>

        <!-- 阿里开源数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.41</version>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!--mapper-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>1.2.4</version>
        </dependency>
        <!--pagehelper-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>

        <!-- mybatis 逆向生成工具  -->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.2</version>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

整合Mybatis

dependencymanagement 无法引入包依赖-CSDN博客
关于Maven依赖dependency无法引入的问题_ 无法引入包-CSDN博客
Maven配置仓库、阿里云镜像、环境变量(史上最全最详细)_maven配置阿里云镜像-CSDN博客

将mybatis-generator-database中的pojo[AppUser+Fans]复制转移到imooc-news-dev-model com/imooc/pojo中
将mybatis-generator-database中的com/imooc/user/mapper[AppUserMapper、FansMapper]复制转移到imooc-news-service-user com/imooc/user/mapper中
所有的服务都是要实现API的
将mybatis-generator-database中的com/imooc/my/mapper[MyMapper]复制转移到imooc-news-dev-service-api com/imooc/my/mapper中
将..database中的resources的mapper[AppUserMapper.xml、FansMapper.xml]转移到...service-user的resources的mapper中
imooc-news-dev-service-user中的resources的application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-user
  datasource: # 数据源的相关配置
    type: com.zaxxer.hikari.HikariDataSource          # 数据源类型:HikariCP
    driver-class-name: com.mysql.jdbc.Driver          # mysql驱动
    url: jdbc:mysql://localhost:3306/imooc-news-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
    username: root
    password: root
    hikari:
      connection-timeout: 30000       # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 默认:30秒
      minimum-idle: 5                 # 最小连接数
      maximum-pool-size: 20           # 最大连接数
      auto-commit: true               # 自动提交
      idle-timeout: 600000            # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
      pool-name: DateSourceHikariCP     # 连接池名字
      max-lifetime: 1800000           # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms
      connection-test-query: SELECT 1
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
############################################################
#
# mybatis 配置
#
############################################################
mybatis:
  type-aliases-package: com.imooc.pojo          # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml      # mapper映射文件

############################################################
#
# mybatis mapper 配置
#
############################################################
# 通用 Mapper 配置
mapper:
  mappers: com.imooc.my.mapper.MyMapper
  not-empty: false    # 在进行数据库操作的的时候,判断表达式 username != null, 是否追加 username != ''
  identity: MYSQL
# 分页插件配置
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true

【解决】SLF4J: Class path contains multiple SLF4J bindings._启动metastore时slf4j: class path contains multiple sl-CSDN博客
整合MongoDB踩坑记录及解决方法 - 会飞的猪仔 - 博客园 (cnblogs.com)

com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan("com.imooc")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Swagger2接口文檔工具的使用

imooc-news-dev-service-api
<!-- swagger2 配置 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${springfox-swagger2.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${springfox-swagger2.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>${swagger-bootstrap-ui.version}</version>
        </dependency>
com/imooc/api/config/Swagger2.java
package com.imooc.api.config;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration //Springboot啓動的時候會被掃描到并且加載
@EnableSwagger2
public class Swagger2 {

    //    http://localhost:8088/swagger-ui.html     原路径
//http://user.imoocnews.com:8003/swagger-ui.htm
    //    http://localhost:8088/doc.html            新路径
//http://user.imoocnews.com:8003/doc.html

    // 配置swagger2核心配置 docket
    @Bean
    public Docket createRestApi() {
        Predicate<RequestHandler> adminPredicate = RequestHandlerSelectors.basePackage("com.imooc.admin.controller");
//        Predicate<RequestHandler> articlePredicate = RequestHandlerSelectors.basePackage("com.imooc.article.controller");
        Predicate<RequestHandler> userPredicate = RequestHandlerSelectors.basePackage("com.imooc.user.controller");
        Predicate<RequestHandler> filesPredicate = RequestHandlerSelectors.basePackage("com.imooc.files.controller");

        return new Docket(DocumentationType.SWAGGER_2)  // 指定api类型为swagger2
                .apiInfo(apiInfo())                 // 用于定义api文档汇总信息
                .select()
                .apis(Predicates.or(userPredicate, adminPredicate, filesPredicate))
//                .apis(Predicates.or(adminPredicate, articlePredicate, userPredicate, filesPredicate))
                .paths(PathSelectors.any())         // 所有controller
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕课新闻·自媒体接口api")                       // 文档页标题
                .contact(new Contact("imooc",
                        "https://www.imooc.com",
                        "abc@imooc.com"))                   // 联系人信息
                .description("专为慕课新闻·自媒体平台提供的api文档")      // 详细信息
                .version("1.0.1")                               // 文档版本号
                .termsOfServiceUrl("https://www.imooc.com")     // 网站地址
                .build();
    }
}
com/imooc/api/controller/user/HelloControllerApi.java
package com.imooc.api.controller.user;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;

@Api(value = "controller的標題",tags = {"xx功能的Controller"})
public interface HelloControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "GET")
    @GetMapping("/hello")
    public Object hello();
}

[maven-之Lifecycle详解_maven lifecycle-CSDN博客](https://blog.csdn.net/qq_39505065/article/details/102915403#第三部分 PS)

Maven中的Lifecycle的install是什麽 ?
将包安装到本地存储库中,作为本地其他项目的依赖项

梳理短信登錄注冊流程

  • 短信登录注册
  • 短信验证码发送与限制
  • 分布式会话
  • 用户信息完善,OSS/FastDFS文件上传
  • AOP日志监控

aliyun.properties
#这里需要去阿里云上购买短信验证 [电脑aliyun_Key.txt有]
aliyun.accessKeyID=
aliyun.accessKeySecret=

SendSms_短信服务_API调试-阿里云OpenAPI开发者门户 (aliyun.com)
短信发送并查询示例_短信服务_示例中心-阿里云OpenAPI开发者门户 (aliyun.com)
RAM 访问控制 (aliyun.com)
短信服务 (aliyun.com)
云服务器管理控制台 (aliyun.com)

【imooc-news-dev-common】
pom.xml
加入springboot依赖包
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-common:
    通用工程
    包含了一些工具类,枚举类,封装的一些公共方法以及一些第三方组件等
-->
    <artifactId>imooc-news-dev-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--    引入SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <!--  第三方云厂商相关依赖 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>
    </dependencies>
</project>
com/imooc/utils/extend/AliyunResource.java
package com.imooc.utils.extend;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:aliyun.properties")
@ConfigurationProperties(prefix = "aliyun") //这里是前缀
public class AliyunResource {
    private String accessKeyID;
    private String accessKeySecret;

    public String getAccessKeyID() {
        return accessKeyID;
    }

    public void setAccessKeyID(String accessKeyID) {
        this.accessKeyID = accessKeyID;
    }

    public String getAccessKeySecret() {
        return accessKeySecret;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }
}
com/imooc/utils/SMSUtils.java
package com.imooc.utils;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.imooc.utils.extend.AliyunResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component //工具类 可以作为组件
public class SMSUtils {
    @Autowired
    public AliyunResource aliyunResource;
    final static Logger logger = LoggerFactory.getLogger(SMSUtils.class);
    public void sendSMS(String mobile, String code) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou",
                aliyunResource.getAccessKeyID(),
                aliyunResource.getAccessKeySecret());
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        request.setSysMethod(MethodType.POST);
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        request.putQueryParameter("RegionId", "cn-hangzhou");
        //给对方发送的手机号
        request.putQueryParameter("PhoneNumbers", mobile);
        request.putQueryParameter("SignName", "小潘科技");//控制台可以添加签名
        request.putQueryParameter("TemplateCode", "SMS_467115116");
        request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");//JSON对象字符串
        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            // 打印阿里云API的响应结果
            logger.info("Aliyun SMS API response: " + response.getData());
        } catch (ServerException e) {
            e.printStackTrace();
            logger.error("ServerException: " + e.getMessage());
        } catch (ClientException e) {
            e.printStackTrace();
            logger.error("ClientException: " + e.getMessage());
        }
    }
}
com/imooc/api/controller/user/PassportControllerApi.java[接口]
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode();
}
com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.SMSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("passport")
public class PassportController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;
    // 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
    //    @Autowired
    //    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(){
        // 生成6位随机验证码
        String random = String.valueOf((int)((Math.random() * 9 + 1) * 100000));
        // 打印生成的验证码以便调试
        logger.info("Generated SMS code: " + random);
//        String random = ((Math.random() * 9 + 1) * 100000) + "";
        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替
        // 记录发送短信的结果(添加日志)
        logger.info("SMS sent to 15027597000 with code: " + random);

        return GraceJSONResult.ok();
    }
}
com/imooc/grace/result/GraceJSONResult.java
package com.imooc.grace.result;

import java.util.Map;

/**
 * 自定义响应数据类型枚举升级版本
 *
 * @Title: IMOOCJSONResult.java
 * @Package com.imooc.utils
 * @Description: 自定义响应数据结构
 *                 本类可提供给 H5/ios/安卓/公众号/小程序 使用
 *                 前端接受此类数据(json object)后,可自行根据业务去实现相关功能
 *
 * @Copyright: Copyright (c) 2020
 * @Company: www.imooc.com
 * @author 慕课网 - 风间影月
 * @version V2.0
 */
public class GraceJSONResult {

    // 响应业务状态码
    private Integer status;

    // 响应消息
    private String msg;

    // 是否成功
    private Boolean success;

    // 响应数据,可以是Object,也可以是List或Map等
    private Object data;

    /**
     * 成功返回,带有数据的,直接往OK方法丢data数据即可
     * @param data
     * @return
     */
    public static GraceJSONResult ok(Object data) {
        return new GraceJSONResult(data);
    }
    /**
     * 成功返回,不带有数据的,直接调用ok方法,data无须传入(其实就是null)
     * @return
     */
    public static GraceJSONResult ok() {
        return new GraceJSONResult(ResponseStatusEnum.SUCCESS);
    }
    public GraceJSONResult(Object data) {
        this.status = ResponseStatusEnum.SUCCESS.status();
        this.msg = ResponseStatusEnum.SUCCESS.msg();
        this.success = ResponseStatusEnum.SUCCESS.success();
        this.data = data;
    }


    /**
     * 错误返回,直接调用error方法即可,当然也可以在ResponseStatusEnum中自定义错误后再返回也都可以
     * @return
     */
    public static GraceJSONResult error() {
        return new GraceJSONResult(ResponseStatusEnum.FAILED);
    }

    /**
     * 错误返回,map中包含了多条错误信息,可以用于表单验证,把错误统一的全部返回出去
     * @param map
     * @return
     */
    public static GraceJSONResult errorMap(Map map) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, map);
    }

    /**
     * 错误返回,直接返回错误的消息
     * @param msg
     * @return
     */
    public static GraceJSONResult errorMsg(String msg) {
        return new GraceJSONResult(ResponseStatusEnum.FAILED, msg);
    }

    /**
     * 错误返回,token异常,一些通用的可以在这里统一定义
     * @return
     */
    public static GraceJSONResult errorTicket() {
        return new GraceJSONResult(ResponseStatusEnum.TICKET_INVALID);
    }

    /**
     * 自定义错误范围,需要传入一个自定义的枚举,可以到[ResponseStatusEnum.java[中自定义后再传入
     * @param responseStatus
     * @return
     */
    public static GraceJSONResult errorCustom(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }
    public static GraceJSONResult exception(ResponseStatusEnum responseStatus) {
        return new GraceJSONResult(responseStatus);
    }

    public GraceJSONResult(ResponseStatusEnum responseStatus) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, Object data) {
        this.status = responseStatus.status();
        this.msg = responseStatus.msg();
        this.success = responseStatus.success();
        this.data = data;
    }
    public GraceJSONResult(ResponseStatusEnum responseStatus, String msg) {
        this.status = responseStatus.status();
        this.msg = msg;
        this.success = responseStatus.success();
    }

    public GraceJSONResult() {
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }
}

安装配置整合Redis

通过Xftp7把redis-5.0.7.tar.gz传入到服务器
[root@iZbp1dssknxftmjczbtpndZ ~]# tar -zxvf redis-5.0.7.tar.gz 
[root@iZbp1dssknxftmjczbtpndZ ~]# ls
apache-zookeeper-3.6.0-bin         rabbitmq-server-3.8.2-1.el7.noarch.rpm
apache-zookeeper-3.6.0-bin.tar.gz  redis-5.0.7
erlang-22.3-1.el7.x86_64.rpm       redis-5.0.7.tar.gz
[root@iZbp1dssknxftmjczbtpndZ ~]# cd redis-5.0.7
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# ll
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# yum install gcc-c++
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# make
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cd /usr/local/ ★
[root@iZbp1dssknxftmjczbtpndZ local]# ll
[root@iZbp1dssknxftmjczbtpndZ local]# pwd
/usr/local
[root@iZbp1dssknxftmjczbtpndZ local]# cd redis/ ★
[root@iZbp1dssknxftmjczbtpndZ redis]# ll
total 4
drwxr-xr-x 2 root root 4096 May 15 11:19 bin
[root@iZbp1dssknxftmjczbtpndZ redis]# cd bin ★
[root@iZbp1dssknxftmjczbtpndZ bin]# ll
total 32772
-rwxr-xr-x 1 root root 4366880 May 15 11:04 redis-benchmark
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-aof
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-rdb
-rwxr-xr-x 1 root root 4807952 May 15 11:04 redis-cli
-rw-r--r-- 1 root root       0 May 15 11:19 redis.conf
lrwxrwxrwx 1 root root      12 May 15 11:04 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-server
[root@iZbp1dssknxftmjczbtpndZ redis]# cd
[root@iZbp1dssknxftmjczbtpndZ ~]# cd redis-5.0.7
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# ls
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cp redis.conf /usr/local/redis/bin/
cp: overwrite ‘/usr/local/redis/bin/redis.conf’? y
[root@iZbp1dssknxftmjczbtpndZ redis-5.0.7]# cd /usr/local/redis/bin/
[root@iZbp1dssknxftmjczbtpndZ bin]# ll
total 32836
-rwxr-xr-x 1 root root 4366880 May 15 11:04 redis-benchmark
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-aof
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-check-rdb
-rwxr-xr-x 1 root root 4807952 May 15 11:04 redis-cli
-rw-r--r-- 1 root root   61797 May 15 11:28 redis.conf
lrwxrwxrwx 1 root root      12 May 15 11:04 redis-sentinel -> redis-server
-rwxr-xr-x 1 root root 8125288 May 15 11:04 redis-server
[root@iZbp1dssknxftmjczbtpndZ bin]# vim redis.conf
在里面 /bind 直接搜索
把bind 127.0.0.1修改成 0.0.0.0在任何地方都可以进行操作修改
在里面 /dae
把daemonize no 改成 daemonize yes[后台启动]
在里面 /require
把requirepass foobared 这里是设置密码 requirepass XXXX
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-server redis.conf ★
32421:C 15 May 2024 11:35:36.687 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
32421:C 15 May 2024 11:35:36.687 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=32421, just started
32421:C 15 May 2024 11:35:36.687 # Configuration loaded
[root@iZbp1dssknxftmjczbtpndZ bin]# ps -ef|grep redis
root     32422     1  0 11:35 ?        00:00:00 ./redis-server 0.0.0.0:6379
root     32456 25226  0 11:35 pts/0    00:00:00 grep --color=auto redis
【此时说明已经成功启动Redis】
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-cli ★
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set name imooc
OK
127.0.0.1:6379> get name
"imooc"
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-cli -p 6379 shutdown

★直接进入redis文件内★
[root@iZbp1dssknxftmjczbtpndZ ~]# cd /usr/local/redis/bin       //进入文件内
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-server redis.conf   //启动
[root@iZbp1dssknxftmjczbtpndZ bin]# ./redis-cli                 //测试
★★

Linux下Redis服务启动与关闭_linux 关闭redis-CSDN博客

安装配置整合Redis-2

下载并安装好 Redis Desktop Manager
新连接设置
名字:redis-imooc-news 47.98.225.105
地址:47.98.225.105:6379

加一下redis的依赖
imooc-news-dev-common
pom.xml
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
<!--
    imooc-news-dev-common:
    通用工程
    包含了一些工具类,枚举类,封装的一些公共方法以及一些第三方组件等
-->
    <artifactId>imooc-news-dev-common</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--    引入SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- 引入 redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </dependency>
        <!-- jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- apache 工具类 -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
        </dependency>
        <!-- google 工具类 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
        <!-- joda-time 时间工具 -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>

        <!--  第三方云厂商相关依赖 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>
    </dependencies>
</project>
com/imooc/utils/RedisOperator.java
package com.imooc.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @Title: Redis 工具类
 * @author 风间影月
 */
@Component
public class RedisOperator {
    
    @Resource
    private StringRedisTemplate redisTemplate;

    // Key(键),简单的key-value操作

    /**
     * 判断key是否存在
     * @param key
     * @return
     */
    public boolean keyIsExist(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
     * 
     * @param key
     * @return
     */
    public long ttl(String key) {
        return redisTemplate.getExpire(key);
    }
    
    /**
     * 实现命令:expire 设置过期时间,单位秒
     * 
     * @param key
     * @return
     */
    public void expire(String key, long timeout) {
        redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }
    
    /**
     * 实现命令:increment key,增加key一次
     * 
     * @param key
     * @return
     */
    public long increment(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 实现命令:decrement key,减少key一次
     *
     * @param key
     * @return
     */
    public long decrement(String key, long delta) {
        return redisTemplate.opsForValue().decrement(key, delta);
    }

    /**
     * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
     */
    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 实现命令:DEL key,删除一个key
     * 
     * @param key
     */
    public void del(String key) {
        redisTemplate.delete(key);
    }

    // String(字符串)

    /**
     * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
     * 
     * @param key
     * @param value
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
     * 
     * @param key
     * @param value
     * @param timeout
     *            (以秒为单位)
     */
    public void set(String key, String value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    /**
     * 如果key不存在,则设置,如果存在,则报错
     * @param key
     * @param value
     */
    public void setnx60s(String key, String value) {
        redisTemplate.opsForValue().setIfAbsent(key, value, 60, TimeUnit.SECONDS);
    }

    /**
     * 如果key不存在,则设置,如果存在,则报错
     * @param key
     * @param value
     */
    public void setnx(String key, String value) {
        redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
     * 实现命令:GET key,返回 key所关联的字符串值。
     * 
     * @param key
     * @return value
     */
    public String get(String key) {
        return (String)redisTemplate.opsForValue().get(key);
    }

    /**
     * 批量查询,对应mget
     * @param keys
     * @return
     */
    public List<String> mget(List<String> keys) {
        return redisTemplate.opsForValue().multiGet(keys);
    }

    /**
     * 批量查询,管道pipeline
     * @param keys
     * @return
     */
    public List<Object> batchGet(List<String> keys) {

//        nginx -> keepalive
//        redis -> pipeline

        List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                StringRedisConnection src = (StringRedisConnection)connection;

                for (String k : keys) {
                    src.get(k);
                }
                return null;
            }
        });

        return result;
    }


    // Hash(哈希表)

    /**
     * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
     * 
     * @param key
     * @param field
     * @param value
     */
    public void hset(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    /**
     * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
     * 
     * @param key
     * @param field
     * @return
     */
    public String hget(String key, String field) {
        return (String) redisTemplate.opsForHash().get(key, field);
    }

    /**
     * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
     * 
     * @param key
     * @param fields
     */
    public void hdel(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }

    /**
     * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
     * 
     * @param key
     * @return
     */
    public Map<Object, Object> hgetall(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    // List(列表)

    /**
     * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
     * 
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long lpush(String key, String value) {
        return redisTemplate.opsForList().leftPush(key, value);
    }

    /**
     * 实现命令:LPOP key,移除并返回列表 key的头元素。
     * 
     * @param key
     * @return 列表key的头元素。
     */
    public String lpop(String key) {
        return (String)redisTemplate.opsForList().leftPop(key);
    }

    /**
     * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
     * 
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long rpush(String key, String value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }
}

Spring Boot集成Redis的坑,踩了! - 知乎 (zhihu.com)
@Autowired和@Resource注解的区别和联系(十分详细,不看后悔)_为什么@resource和@autowired 注入的对象不一样-CSDN博客

妈的有个超级大bug 整我一下午,
@Component public class RedisOperator {
@Autowired private StringRedisTemplate redisTemplate;}
报错信息 Could not autowire. No beans of ‘StringRedisTemplate’ type found.

在这里不要本末倒置 回归最原始的报错 那就是pom.xml中的导包依赖问题
有的时候直接复制的项目中的成熟依赖 根据时代的不同可能会导致丢失无法下载依赖
这时要去百度Maven库手动下载 并且手动添加 然后手动导入Project Structure → Libraries 手动导入自己需要的包[记住包的版本 也要在依赖里面体现 < version >],而且如果有红色波浪线的包可以删除 再重新导入即可

Spring里遇到的一个问题,autowired时报找不到bean定义_autowired找不到bean-CSDN博客

完善发送短信接口

com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.HttpServletRequest;

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request);
}
com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;


import com.imooc.api.controller.user.BaseController;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.RedisOperator;
import com.imooc.utils.SMSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.imooc.utils.IPUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("passport")
public class PassportController extends BaseController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;
// 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
//    @Autowired
//    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request){
        String userIp = IPUtil.getRequestIp(request);
        //根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码
//        redis.setnx60s("smscode"+ip);
        redis.setnx60s(MOBILE_SMSCODE + ":"+userIp,userIp);

        //生成随机验证码并且发送短信
        String random = ((Math.random() * 9 + 1) * 100000) + "";
        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替

        //把验证码存入redis,用于后续进行验证
        redis.set(MOBILE_SMSCODE+":"+mobile, random, 30*60);
        return GraceJSONResult.ok();
    }
}
com/imooc/api/controller/user/BaseController.java
package com.imooc.api.controller.user;

import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;

public class BaseController {
   @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";

}
com/imooc/utils/IPUtil.java
package com.imooc.utils;

import javax.servlet.http.HttpServletRequest;

/**
 * 用户获得用户ip的工具类
 */
public class IPUtil {

    /**
     * 获取请求IP:
     * 用户的真实IP不能使用request.getRemoteAddr()
     * 这是因为可能会使用一些代理软件,这样ip获取就不准确了
     * 此外我们如果使用了多级(LVS/Nginx)反向代理的话,ip需要从X-Forwarded-For中获得第一个非unknown的IP才是用户的有效ip。
     * @param request
     * @return
     */
    public static String getRequestIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

快速使用 在控制台发送短信_短信服务(SMS)-阿里云帮助中心 (aliyun.com)

联调前端发送短信, 解决跨域问题

http://writer.imoocnews.com:9090/imooc-news/writer/passport.html
因为后台写死了手机号 所以在输入手机号可以随便 点击发送验证码后 会在浏览器控制台输出跨域问题 在后端要設置允許跨域請求
-----------------------------------------------------------------------------------
passport.html:1  Access to XMLHttpRequest at 'http://user.imoocnews.com:8003/passport/getSMSCode?mobile=123334323' from origin 'http://writer.imoocnews.com:9090' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

GET http://user.imoocnews.com:8003/passport/getSMSCode?mobile=123334323 net::ERR_FAILED 200 (OK)
axios.min.js:2  Uncaught (in promise) Error: Network Error
    at e.exports (axios.min.js:2:9633)
    at l.onerror (axios.min.js:2:8398)
...service-api  com/imooc/api/config/CorsConfig.java
package com.imooc.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration //SpringBoot可以加载该信息
public class CorsConfig {

    public CorsConfig() {
    }

    @Bean
    public CorsFilter corsFilter() {
        // 1. 添加cors配置信息
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*");
        // 设置是否发送cookie信息
        config.setAllowCredentials(true);
        // 设置允许请求的方式
        config.addAllowedMethod("*");
        // 设置允许的header
        config.addAllowedHeader("*");
        // 2. 为url添加映射路径
        UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
        corsSource.registerCorsConfiguration("/**", config);
        // 3. 返回重新定义好的corsSource
        return new CorsFilter(corsSource);
    }
}
com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;


import com.imooc.api.BaseController;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.SMSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("passport")
public class PassportController extends BaseController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;
    // 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
    //    @Autowired
    //    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request){
        //获取用户ip
        String userIp = IPUtil.getRequestIp(request);
        logger.info("User ip:", userIp);
        //根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, userIp);

        // 生成6位随机验证码
        String random = (int)((Math.random() * 9 + 1) * 100000) + "";
        // 打印生成的验证码以便调试
//        logger.info("Generated SMS code: " + random);
//        String random = ((Math.random() * 9 + 1) * 100000) + "";
//        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替
        // 记录发送短信的结果(添加日志)
//        logger.info("SMS sent to 15027597319 with code: " + random);
        redis.set(MOBILE_SMSCODE + ":" + mobile, random, 30 * 60);
   //记得如果要发送到redis中 则需要先用application-dev.yml导入RedisDesktopManager正确的网络地址127.0.0.1 
        return GraceJSONResult.ok();
    }
}
com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);
}

拦截并限制60秒用户短信发送

service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/password/getSMSCode"); //拦截PassportControllerApi里的信息
    }
}
com/imooc/api/interceptors/PassportInterceptor.java
package com.imooc.api.interceptors;

import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

    public class PassportInterceptor implements HandlerInterceptor {

        @Autowired
        public RedisOperator redis;

        public static final String MOBILE_SMSCODE = "mobile:smscode";

        /**
         * 拦截请求,访问controller之前
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

            // 获得用户ip
            String userIp = IPUtil.getRequestIp(request);

            boolean keyIsExist = redis.keyIsExist(MOBILE_SMSCODE + ":" + userIp);

            if (keyIsExist) {
                GraceException.display(ResponseStatusEnum.SMS_NEED_WAIT_ERROR);
//            System.out.println("短信发送频率太大!");
                return false;
            }

            /**
             * false:请求被拦截
             * true:请求通过验证,放行
             */
            return true;
        }


        /**
         * 请求访问到controller之后,渲染视图之前
         * @param request
         * @param response
         * @param handler
         * @param modelAndView
         * @throws Exception
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        }

        /**
         * 请求访问到controller之后,渲染视图之后
         * @param request
         * @param response
         * @param handler
         * @param ex
         * @throws Exception
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        }
    }
dev-common com/imooc/exception/GraceException.java
package com.imooc.exception;

import com.imooc.grace.result.ResponseStatusEnum;

/**
 * 优雅的处理异常,统一封装
 */
public class GraceException {

    public static void display(ResponseStatusEnum responseStatusEnum) {
        throw new MyCustomException(responseStatusEnum);
    }

}
com/imooc/exception/MyCustomException.java
package com.imooc.exception;

import com.imooc.grace.result.ResponseStatusEnum;

/**
 * 自定义异常
 * 目的:统一处理异常信息
 *      便于解耦,service与controller错误的解耦,不会被service返回的类型而限制
 */
public class MyCustomException extends RuntimeException {

    private ResponseStatusEnum responseStatusEnum;

    public MyCustomException(ResponseStatusEnum responseStatusEnum) {
        super("异常状态码为:" + responseStatusEnum.status()
                + ";具体异常信息为:" + responseStatusEnum.msg());
        this.responseStatusEnum = responseStatusEnum;
    }

    public ResponseStatusEnum getResponseStatusEnum() {
        return responseStatusEnum;
    }

    public void setResponseStatusEnum(ResponseStatusEnum responseStatusEnum) {
        this.responseStatusEnum = responseStatusEnum;
    }
}

自定义异常, 返回错误信息

[接上方GraceException、MyCustomException、PassportInterceptor]
dev-common  com/imooc/grace/result/ResponseStatusEnum.java
package com.imooc.grace.result;

/**
 * 响应结果枚举,用于提供给GraceJSONResult返回给前端的
 * 本枚举类中包含了很多的不同的状态码供使用,可以自定义
 * 便于更优雅的对状态码进行管理,一目了然
 */
public enum ResponseStatusEnum {

    SUCCESS(200, true, "操作成功!"),
    FAILED(500, false, "操作失败!"),

    // 50x
    UN_LOGIN(501,false,"请登录后再继续操作!"),
    TICKET_INVALID(502,false,"会话失效,请重新登录!"),
    NO_AUTH(503,false,"您的权限不足,无法继续操作!"),
    MOBILE_ERROR(504,false,"短信发送失败,请稍后重试!"),
    SMS_NEED_WAIT_ERROR(505,false,"短信发送太快啦~请稍后再试!"),
    SMS_CODE_ERROR(506,false,"验证码过期或不匹配,请稍后再试!"),
    USER_FROZEN(507,false,"用户已被冻结,请联系管理员!"),
    USER_UPDATE_ERROR(508,false,"用户信息更新失败,请联系管理员!"),
    USER_INACTIVE_ERROR(509,false,"请前往[账号设置]修改信息激活后再进行后续操作!"),
    FILE_UPLOAD_NULL_ERROR(510,false,"文件不能为空,请选择一个文件再上传!"),
    FILE_UPLOAD_FAILD(511,false,"文件上传失败!"),
    FILE_FORMATTER_FAILD(512,false,"文件图片格式不支持!"),
    FILE_MAX_SIZE_ERROR(513,false,"仅支持500kb大小以下的图片上传!"),
    FILE_NOT_EXIST_ERROR(514,false,"你所查看的文件不存在!"),
    USER_STATUS_ERROR(515,false,"用户状态参数出错!"),
    USER_NOT_EXIST_ERROR(516,false,"用户不存在!"),

    // 自定义系统级别异常 54x
    SYSTEM_INDEX_OUT_OF_BOUNDS(541, false, "系统错误,数组越界!"),
    SYSTEM_ARITHMETIC_BY_ZERO(542, false, "系统错误,无法除零!"),
    SYSTEM_NULL_POINTER(543, false, "系统错误,空指针!"),
    SYSTEM_NUMBER_FORMAT(544, false, "系统错误,数字转换异常!"),
    SYSTEM_PARSE(545, false, "系统错误,解析异常!"),
    SYSTEM_IO(546, false, "系统错误,IO输入输出异常!"),
    SYSTEM_FILE_NOT_FOUND(547, false, "系统错误,文件未找到!"),
    SYSTEM_CLASS_CAST(548, false, "系统错误,类型强制转换错误!"),
    SYSTEM_PARSER_ERROR(549, false, "系统错误,解析出错!"),
    SYSTEM_DATE_PARSER_ERROR(550, false, "系统错误,日期解析出错!"),

    // admin 管理系统 56x
    ADMIN_USERNAME_NULL_ERROR(561, false, "管理员登录名不能为空!"),
    ADMIN_USERNAME_EXIST_ERROR(562, false, "管理员登录名已存在!"),
    ADMIN_NAME_NULL_ERROR(563, false, "管理员负责人不能为空!"),
    ADMIN_PASSWORD_ERROR(564, false, "密码不能为空后者两次输入不一致!"),
    ADMIN_CREATE_ERROR(565, false, "添加管理员失败!"),
    ADMIN_PASSWORD_NULL_ERROR(566, false, "密码不能为空!"),
    ADMIN_NOT_EXIT_ERROR(567, false, "管理员不存在或密码错误!"),
    ADMIN_FACE_NULL_ERROR(568, false, "人脸信息不能为空!"),
    ADMIN_FACE_LOGIN_ERROR(569, false, "人脸识别失败,请重试!"),
    CATEGORY_EXIST_ERROR(570, false, "文章分类已存在,请换一个分类名!"),

    // 媒体中心 相关错误 58x
    ARTICLE_COVER_NOT_EXIST_ERROR(580, false, "文章封面不存在,请选择一个!"),
    ARTICLE_CATEGORY_NOT_EXIST_ERROR(581, false, "请选择正确的文章领域!"),
    ARTICLE_CREATE_ERROR(582, false, "创建文章失败,请重试或联系管理员!"),
    ARTICLE_QUERY_PARAMS_ERROR(583, false, "文章列表查询参数错误!"),
    ARTICLE_DELETE_ERROR(584, false, "文章删除失败!"),
    ARTICLE_WITHDRAW_ERROR(585, false, "文章撤回失败!"),
    ARTICLE_REVIEW_ERROR(585, false, "文章审核出错!"),
    ARTICLE_ALREADY_READ_ERROR(586, false, "文章重复阅读!"),

    // 人脸识别错误代码
    FACE_VERIFY_TYPE_ERROR(600, false, "人脸比对验证类型不正确!"),
    FACE_VERIFY_LOGIN_ERROR(601, false, "人脸登录失败!"),

    // 系统错误,未预期的错误 555
    SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),
    SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),
    SYSTEM_RESPONSE_NO_INFO(557, false, "");


    // 响应业务状态
    private Integer status;
    // 调用是否成功
    private Boolean success;
    // 响应消息,可以为成功或者失败的消息
    private String msg;

    ResponseStatusEnum(Integer status, Boolean success, String msg) {
        this.status = status;
        this.success = success;
        this.msg = msg;
    }

    public Integer status() {
        return status;
    }
    public Boolean success() {
        return success;
    }
    public String msg() {
        return msg;
    }
}
dev-common  com/imooc/exception/GraceExceptionHandler.java
package com.imooc.exception;

import com.imooc.grace.result.GraceJSONResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 统一异常拦截处理
 * 可以针对异常的类型进行捕获 然后返回json信息到前端
 */
@ControllerAdvice //本质上是实现AOP的管理
public class GraceExceptionHandler {
    @ExceptionHandler(MyCustomException.class)
    //只要是这个类的异常都会进入下面的方法
    @ResponseBody
    public GraceJSONResult returnMyException(MyCustomException e){
        e.printStackTrace(); //打印信息
        return GraceJSONResult.exception(e.getResponseStatusEnum());
    }
}

验证BO信息(注册登录接口)

service-api  com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; //用户需求验证

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);

    @ApiOperation(value = "一键注册登录接口",notes = "一键注册登录接口",httpMethod = "POST")
    @PostMapping("/doLogin") //表单里面用post  RequestBody后面传过来的东西和json对象对应
    public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO, BindingResult result);
}
dev-model  com/imooc/pojo/bo/RegistLoginBO.java
package com.imooc.pojo.bo;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

//加上@data 会自动生成getter+setter
public class RegistLoginBO {
    //不为空 空的话可以返回 不用NOTNULL因为无法校验空字符串 用NotBlank
    @NotBlank(message = "手机号不能为空") 
    private String mobile;
    @NotBlank(message = "短信验证码不能为空")
    private String smsCode;

    @Override
    public String toString() {
        return "RegistLoginBO{" +
                "mobile='" + mobile + '\'' +
                ", smsCode='" + smsCode + '\'' +
                '}';
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getSmsCode() {
        return smsCode;
    }

    public void setSmsCode(String smsCode) {
        this.smsCode = smsCode;
    }
}
service-user  com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;


import com.imooc.api.BaseController;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.utils.IPUtil;
import com.imooc.utils.SMSUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("passport")
public class PassportController extends BaseController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;
    // 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
    //    @Autowired
    //    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request){
        //获取用户ip
        String userIp = IPUtil.getRequestIp(request);
        logger.info("User ip:", userIp);
        //根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, userIp);

        // 生成6位随机验证码
        String random = (int)((Math.random() * 9 + 1) * 100000) + "";
        // 打印生成的验证码以便调试
//        logger.info("Generated SMS code: " + random);
//        String random = ((Math.random() * 9 + 1) * 100000) + "";
//        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替
        // 记录发送短信的结果(添加日志)
//        logger.info("SMS sent to 15027597319 with code: " + random);
        redis.set(MOBILE_SMSCODE + ":" + mobile, random, 30 * 60);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult doLogin(RegistLoginBO registLoginBO, BindingResult result) {
        //0.判断BindingResult中是否保存了错误的验证信息 如果有则需要返回
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        String mobile = registLoginBO.getMobile();
        String smsCode = registLoginBO.getSmsCode();

        //1.校验验证码是否匹配[在redis中去获取]
        String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile); //为空||不同值
        if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }
        return GraceJSONResult.ok();
    }
}
service-api  com/imooc/api/BaseController.java
package com.imooc.api;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class BaseController {
   @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";

    /**可以公用 就放到BaseController里面
     * 在任何controller中都可以调用和使用
     * 获取BO中的错误信息
     *
     * @param result
     * @return
     */
    public Map<String, String> getErrors(BindingResult result){
        //对应着RegistLoginBO的信息
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList){
            //发生验证错误所对应的某个属性
            String field = error.getField();
            //验证的错误信息
            String msg = error.getDefaultMessage();
            map.put(field, msg);
        }
        return map;
    }
}
http://writer.imoocnews.com:8003/doc.html   打开校验
POST:/passport/doLogin

{
    "mobile":"",
    "smsCode":""
}

{
"status": 500,
"msg": "操作失败!",
"success": false,
-"data": {
"smsCode": "短信验证码不能为空",
"mobile": "手机号不能为空"
}
}
--------------------------------------------------------
 //不为空 空的话可以返回 不用NOTNULL因为无法校验空字符串 用NotBlank
    @NotBlank(message = "手机号不能为空") 
    private String mobile;
    @NotBlank(message = "短信验证码不能为空")
    private String smsCode;
// 要注意上面的为NotBlank 不然它验证的结果会跳过手机号判断 直接说验证码错误 
// 因为NotNull在处理"mobile":"", "smsCode":""的时候空字符串也算入不为空
//NotBlank兼顾NotNull

通过数据库 查询老用户_老用户添加

service-api  com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; //用户需求验证

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);

    @ApiOperation(value = "一键注册登录接口",notes = "一键注册登录接口",httpMethod = "POST")
    @PostMapping("/doLogin") //表单里面用post  RequestBody后面传过来的东西和json对象对应
    public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO, BindingResult result);
}
service-user  com/imooc/user/controller/PassportController.java
package com.imooc.user.controller;


import com.imooc.api.BaseController;
import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.enums.UserStatus;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.IPUtil;
import com.imooc.utils.SMSUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("passport")
public class PassportController extends BaseController implements PassportControllerApi {
    final static Logger logger = LoggerFactory.getLogger(PassportController.class);

    @Autowired
    private SMSUtils smsUtils;

    @Autowired
    private UserService userService;
    // 这里去除的原因是因为新建了一个BaseController 在里面有信息 且在这加个extends
    //    @Autowired
    //    private RedisOperator redis;

    @Override
    public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request){
        //获取用户ip
        String userIp = IPUtil.getRequestIp(request);
        logger.info("User ip:", userIp);
        //根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码
        redis.setnx60s(MOBILE_SMSCODE + ":" + userIp, userIp);

        // 生成6位随机验证码
        String random = (int)((Math.random() * 9 + 1) * 100000) + "";
        // 打印生成的验证码以便调试
//        logger.info("Generated SMS code: " + random);
//        String random = ((Math.random() * 9 + 1) * 100000) + "";
//        smsUtils.sendSMS("15027597319",random);//可以用MyInfo.getMobile代替
        // 记录发送短信的结果(添加日志)
//        logger.info("SMS sent to 15027597319 with code: " + random);
        redis.set(MOBILE_SMSCODE + ":" + mobile, random, 30 * 60);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult doLogin(RegistLoginBO registLoginBO, BindingResult result) {
        //0.判断BindingResult中是否保存了错误的验证信息 如果有则需要返回
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        String mobile = registLoginBO.getMobile();
        String smsCode = registLoginBO.getSmsCode();

        //1.校验验证码是否匹配[在redis中去获取]
        String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile); //为空||不同值
        if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }

        //2.查询数据库,判断该用户注册
        AppUser user = userService.queryMobileIsExist(mobile);
        if (user != null && user.getActiveStatus() == UserStatus.FROZEN.type){
            //如果用户不为空,并且状态为冻结,则直接抛出异常,禁止登录
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);
        }else if (user == null){
            //如果用户没有注册过,则为null,需要注册信息入库
            user = userService.createUser(mobile);
        }
        return GraceJSONResult.ok(user);
    }
}
service-user  com/imooc/user/service/impl/UserService.java[接口]
package com.imooc.user.service.impl;

import com.imooc.pojo.AppUser;

public interface UserService {
    /**
     * 判断用户是否存在,如果存在返回user信息
     */
    public AppUser queryMobileIsExist(String mobile);

    /**
     * 创建用户,新增用户记录到数据库
     */
    public AppUser createUser(String mobile);
}
service-user  com/imooc/user/service/UserServiceimpl.java
package com.imooc.user.service;

import com.imooc.enums.Sex;
import com.imooc.enums.UserStatus;
import com.imooc.pojo.AppUser;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.DesensitizationUtil;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
import com.imooc.utils.DateUtil;

import java.util.Date;

@Service
public class UserServiceimpl implements UserService {
    @Autowired
    public AppUserMapper appUserMapper; //基本的CRUD都可以

    @Autowired
    public Sid sid;

    private static final String USER_FACE0 = "https://raw.githubusercontent.com/P-luminary/images/10d94134b65e13cc8ec9b8a9aeae4f958921cab7/data/Imooc_Cat.jpg";
    private static final String USER_FACE1 = "https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg";
    @Override
    public AppUser queryMobileIsExist(String mobile) {
        Example userExample = new Example(AppUser.class);
        Example.Criteria userCriteria = userExample.createCriteria();
        userCriteria.andEqualTo("mobile", mobile);
        AppUser user = appUserMapper.selectOneByExample(userExample);
        return null;
    }

    @Transactional //对整个类的方法,事务起作用。无异常时正常提交,有异常时数据回滚
    @Override
    public AppUser createUser(String mobile) {
        /**
         * 互联网项目都要考虑可扩展性
         * 如果未来的业务激增,那么就需要分表分库
         * 那么数据库表主键id必须保证全局(全库)唯一,不得重复
         */
        String userId = sid.nextShort();
        AppUser user = new AppUser();
        user.setId(userId);
        user.setMobile(mobile);
        user.setNickname("用户:" + DesensitizationUtil.commonDisplay(mobile)); //給手机号加** 是脱敏操作
        user.setFace(USER_FACE1);
        user.setBirthday(DateUtil.stringToDate("2024-06-29")); //字符串转换Date类型
        user.setSex(Sex.secret.type);
        user.setActiveStatus(UserStatus.INACTIVE.type);//是否激活
        user.setTotalIncome(0);//收入
        user.setCreatedTime(new Date());
        user.setUpdatedTime(new Date());
        return user;
    }
}
dev-model  com/imooc/pojo/AppUser.java
package com.imooc.pojo;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;

@Table(name = "app_user")
public class AppUser {
    @Id
    private String id;

    /**
     * 手机号
     */
    private String mobile;

    /**
     * 昵称,媒体号
     */
    private String nickname;

    /**
     * 头像
     */
    private String face;

    /**
     * 真实姓名
     */
    private String realname;

    /**
     * 邮箱地址
     */
    private String email;

    /**
     * 性别 1:男  0:女  2:保密
     */
    private Integer sex;

    /**
     * 生日
     */
    private Date birthday;

    /**
     * 省份
     */
    private String province;

    /**
     * 城市
     */
    private String city;

    /**
     * 区县
     */
    private String district;

    /**
     * 用户状态:0:未激活。 1:已激活:基本信息是否完善,真实姓名,邮箱地址,性别,生日,住址等,如果没有完善,则用户不能发表评论,不能点赞,不能关注。2:已冻结。
     */
    @Column(name = "active_status")
    private Integer activeStatus;

    /**
     * 累计已结算的收入金额,也就是已经打款的金额,每次打款后再此累加
     */
    @Column(name = "total_income")
    private Integer totalIncome;

    /**
     * 创建时间 创建时间
     */
    @Column(name = "created_time")
    private Date createdTime;

    /**
     * 更新时间 更新时间
     */
    @Column(name = "updated_time")
    private Date updatedTime;
}Getter + Setter
service-user  com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan(basePackages = {"com.imooc","com.imooc.utils"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
【来自Chatgpt】
java.sql.SQLException: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'READ' at line 1
    at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.readErrorPacket(AbstractQueryProtocol.java:1594)
    at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.readPacket(AbstractQueryProtocol.java:1453)
    at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.getResult(AbstractQueryProtocol.java:1415)

这个新的错误信息表明在设置 tx_isolation 时,语法存在问题。我们需要确保设置事务隔离级别的语法是正确的。以下是一些更具体的步骤来解决这个问题:

1. 确认MariaDB版本支持的语法
在MariaDB中,设置事务隔离级别的正确语法可能与MySQL略有不同。确保使用MariaDB支持的正确语法。

2. 确认正确的事务隔离级别语法
根据MariaDB的官方文档,设置事务隔离级别的正确方法是使用以下语法:

sql
复制代码
SET SESSION tx_isolation = 'READ-COMMITTED';
3. 修改连接URL
在连接URL中正确设置事务隔离级别:

yaml
复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/imooc-news-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&sessionVariables=tx_isolation='READ-COMMITTED'
4. 确认数据源属性配置
确保数据源属性配置正确:

yaml
复制代码
spring:
  datasource:
    hikari:
      connection-timeout: 30000
      minimum-idle: 5
      maximum-pool-size: 20
      auto-commit: true
      idle-timeout: 600000
      pool-name: DateSourceHikariCP
      max-lifetime: 1800000
      connection-test-query: SELECT 1
      data-source-properties:
        tx_isolation: 'READ-COMMITTED'
5. 确认使用MariaDB JDBC驱动
确保使用MariaDB的JDBC驱动:

Maven
xml
复制代码
<dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>2.7.2</version>
</dependency>
Gradle
groovy
复制代码
implementation 'org.mariadb.jdbc:mariadb-java-client:2.7.2'
6. 测试事务隔离设置
在MariaDB中手动测试设置事务隔离级别,确保语法正确:

sql
复制代码
SET SESSION tx_isolation = 'READ-COMMITTED';
如果这条命令可以成功执行,那么它也应该能在你的连接URL中正确设置。

7. 确认Spring配置
确保Spring Boot配置正确,可以通过显式设置数据源属性来避免问题:

yaml
复制代码
spring:
  datasource:
    hikari:
      data-source-properties:
        tx_isolation: 'READ-COMMITTED'
总结
通过上述步骤,你应该能够解决 tx_isolation 变量设置的问题,并确保连接到MariaDB数据库时没有语法错误。如果问题依然存在,请提供更多的错误日志和配置文件信息,以便进一步诊断。
http://writer.imoocnews.com:8003/doc.html 先接收验证码 通过redis获得 再测试登录接口
{
    "mobile":"15027597319",
    "smsCode":"815850"
}

相应内容:
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    -"data": {
    "id": "240629F0PD4PZANC",
    "mobile": "15027597319",
    "nickname": "用户:15******319",
    "face": "https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg",
    "realname": null,
    "email": null,
    "sex": 2,
    "birthday": "2024-06-29 00:00:00",
    "province": null,
    "city": null,
    "district": null,
    "activeStatus": 0,
    "totalIncome": 0,
    "createdTime": "2024-06-29 19:39:10",
    "updatedTime": "2024-06-29 19:39:10"
    }
}

此时去数据库imooc-news-dev的app_user中发现并未有数据新增进入
再UserServiceimpl.java中
appUserMapper.insert(user);

当如果把app_user数据库离的active_status 的0变成2 就会被冻结【UserStatus】
{
"status": 507,
"msg": "用户已被冻结,请联系管理员!",
"success": false,
"data": null
}

设置会话与cookie信息【注册登录】

service-user  com/imooc/user/controller/PassportController.java
...
 // 3.保存用户分布式会话的相关操作
        int userActiveStatus = user.getActiveStatus();
        if (userActiveStatus != UserStatus.FROZEN.type){
            String uToken = UUID.randomUUID().toString();
            redis.set(REDIS_USER_TOKEN+":"+user.getId(),uToken);//BaseController里面 保存token到redis
            //保存用户id和token到cookie中 设计一个request response 回到PassportControllerApi

        }
        return GraceJSONResult.ok(user);
    }
...
service-api  com/imooc/api/BaseController.java [增加一个setCookie]
package com.imooc.api;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class BaseController {
   @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写
    public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;

    /**可以公用 就放到BaseController里面
     * 在任何controller中都可以调用和使用
     * 获取BO中的错误信息
     *
     * @param result
     * @return
     */
    public Map<String, String> getErrors(BindingResult result){
        //对应着RegistLoginBO的信息
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList){
            //发生验证错误所对应的某个属性
            String field = error.getField();
            //验证的错误信息
            String msg = error.getDefaultMessage();
            map.put(field, msg);
        }
        return map;
    }

/*    public void setCookie(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge){
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8");
            Cookie cookie = new Cookie(cookieName,cookieValue);
            cookie.setMaxAge(maxAge);
            cookie.setDomain("imoocnews.com");
            cookie.setPath("/");//都用cookie
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    } */

   public void setCookie(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge){
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8");
//            Cookie cookie = new Cookie(cookieName,cookieValue);
//            cookie.setMaxAge(maxAge);
//            cookie.setDomain("imoocnews.com");
//            cookie.setPath("/");//都用cookie
            setCookieValue(request, response, cookieName, cookieValue, maxAge);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public void setCookieValue(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge){
            Cookie cookie = new Cookie(cookieName,cookieValue);
            cookie.setMaxAge(maxAge);
            cookie.setDomain("imoocnews.com");
            cookie.setPath("/");//都用cookie
            response.addCookie(cookie);//把cookie传入
        }
}
service-api  com/imooc/api/controller/user/PassportControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; //用户需求验证

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);

    @ApiOperation(value = "一键注册登录接口",notes = "一键注册登录接口",httpMethod = "POST")
    @PostMapping("/doLogin") //表单里面用post  RequestBody后面传过来的东西和json对象对应
    public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO
            , BindingResult result, HttpServletRequest request, HttpServletResponse response);
    //完成之后 去BaseController里面写一个setCookie()方便都可以用
}
service-user  com/imooc/user/controller/PassportController.java
    @Override
    public GraceJSONResult doLogin(RegistLoginBO registLoginBO, BindingResult result, HttpServletRequest request, HttpServletResponse response) {
        //0.判断BindingResult中是否保存了错误的验证信息 如果有则需要返回
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        String mobile = registLoginBO.getMobile();
        String smsCode = registLoginBO.getSmsCode();

        //1.校验验证码是否匹配[在redis中去获取]
        String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile); //为空||不同值
        if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }

        //2.查询数据库,判断该用户注册
        AppUser user = userService.queryMobileIsExist(mobile);
        if (user != null && user.getActiveStatus() == UserStatus.FROZEN.type){
            //如果用户不为空,并且状态为冻结,则直接抛出异常,禁止登录
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);
        }else if (user == null){
            //如果用户没有注册过,则为null,需要注册信息入库
            user = userService.createUser(mobile);
        }

        // 3.保存用户分布式会话的相关操作
        int userActiveStatus = user.getActiveStatus();
        if (userActiveStatus != UserStatus.FROZEN.type){
            String uToken = UUID.randomUUID().toString();
            redis.set(REDIS_USER_TOKEN+":"+user.getId(),uToken);//BaseController里面 保存token到redis

            //保存用户id和token到cookie中 设计一个request response 回到PassportControllerApi
            setCookie(request, response,"uToken",uToken,COOKIE_MONTH);
            setCookie(request, response,"uid",user.getId(),COOKIE_MONTH);
        }
        // 4.用户登录或注册成功以后,需要删除redis中的短信验证码,验证码只能使用一次,用过则作废
        redis.del(MOBILE_SMSCODE + ":" + mobile);
        // 5.返回用户状态 返回前端看
        return GraceJSONResult.ok(userActiveStatus);
    }

资源属性与常量绑定 [优雅]

把这种属性放到常量文件里进行绑定  cookie.setDomain("imoocnews.com");
service-api  com/imooc/api/BaseController.java
public abstract class BaseController {
    @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写
    public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;
 ★ @Value("${website.domain-name}") ★★
 ★ public String DOMAIN_NAME; ★★
...
 public void setCookie(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge) {
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8");
//            Cookie cookie = new Cookie(cookieName,cookieValue);
//            cookie.setMaxAge(maxAge);
//            cookie.setDomain("imoocnews.com");
//            cookie.setPath("/");//都用cookie
            setCookieValue(request, response, cookieName, cookieValue, maxAge);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public void setCookieValue(HttpServletRequest request,
                               HttpServletResponse response,
                               String cookieName,
                               String cookieValue,
                               Integer maxAge) {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setMaxAge(maxAge);
//        cookie.setDomain("imoocnews.com");
        cookie.setDomain(DOMAIN_NAME);
        cookie.setPath("/");//都用cookie
        response.addCookie(cookie);//把cookie传入
    }
...
================================================================================
application-dev.yml
server:
  port: 8003

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379

# setup CN from java, This is resource
website:
  domain-name: imoocnews.com

查询用户账户信息

service-api  com/imooc/api/controller/user/UserControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
public interface UserControllerApi {
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);
}
service-user  com/imooc/user/controller/UserController.java
package com.imooc.user.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.api.controller.user.UserControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.vo.UserAccountInfoVO;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class UserController implements UserControllerApi {
    final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    @Override
    public GraceJSONResult getAccountInfo(String userId) {
        // 0. 判断参数不为空
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);
        }

        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        UserAccountInfoVO accountInfoVO = new UserAccountInfoVO();
        BeanUtils.copyProperties(user, accountInfoVO); //拷贝信息
        return GraceJSONResult.ok(accountInfoVO);
    }
    private AppUser getUser(String userId){
        // TODO 本方法后续公用,并且扩展
        AppUser user = userService.getUser(userId);
        return user;
    }
}
service-user  com/imooc/user/service/impl/UserService.java
package com.imooc.user.service.impl;

import com.imooc.pojo.AppUser;

public interface UserService {
    /**
     * 判断用户是否存在,如果存在返回user信息
     */
    public AppUser queryMobileIsExist(String mobile);

    /**
     * 创建用户,新增用户记录到数据库
     */
    public AppUser createUser(String mobile);

    /**
     * 根据用户主键id查询用户信息
     * @param userId
     * @return
     */
    public AppUser getUser(String userId);
}
====================================================================
service-user  com/imooc/user/service/UserServiceimpl.java
     @Override
    public AppUser getUser(String userId) {
        return appUserMapper.selectByPrimaryKey(userId);
    }
dev-model  com/imooc/pojo/vo/UserAccountInfoVO.java
public class UserAccountInfoVO {
    private String id;
    private String mobile;
    private String nickname;
    private String face;
    private String realname;
    private String email;
    private Integer sex;
    private Date birthday;
    private String province;
    private String city;
    private String district;
}Getter + Setter

信息校验

service-user  com/imooc/user/controller/UserController.java
package com.imooc.user.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.api.controller.user.UserControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.pojo.vo.UserAccountInfoVO;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

@RestController
public class UserController extends BaseController implements UserControllerApi {
    final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    @Override
    public GraceJSONResult getAccountInfo(String userId) {
        // 0. 判断参数不为空
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);
        }

        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        UserAccountInfoVO accountInfoVO = new UserAccountInfoVO();
        BeanUtils.copyProperties(user, accountInfoVO); //拷贝信息
        return GraceJSONResult.ok(accountInfoVO);
    }

    private AppUser getUser(String userId){
        // TODO 本方法后续公用,并且扩展
        AppUser user = userService.getUser(userId);
        return user;
    }

    @Override
    public GraceJSONResult updateUserInfo(UpdateUserInfoBO updateUserInfoBO, BindingResult result) {
        // 0.校验BO
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        // 1.执行更新操作
        return GraceJSONResult.ok();
    }
}
service-api  com/imooc/api/controller/user/UserControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
public interface UserControllerApi {
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
                                          BindingResult result);
}
dev-model  com/imooc/pojo/bo/UpdateUserInfoBO.java
public class UpdateUserInfoBO {

    @NotBlank(message = "用户ID不能为空")
    private String id;

    @NotBlank(message = "用户昵称不能为空")
    @Length(max = 12, message = "用户昵称不能超过12位")
    private String nickname;

    @NotBlank(message = "用户头像不能为空")
    private String face;

    @NotBlank(message = "真实姓名不能为空")
    private String realname;

    @Email
    @NotBlank(message = "邮件不能为空")
    private String email;

    @NotNull(message = "请选择一个性别")
    @Min(value = 0, message = "性别选择不正确")
    @Max(value = 1, message = "性别选择不正确")
    private Integer sex;

    @NotNull(message = "请选择生日日期")
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") // 解决前端日期字符串传到后端后,转换为Date类型
    private Date birthday;

    @NotBlank(message = "请选择所在城市")
    private String province;

    @NotBlank(message = "请选择所在城市")
    private String city;

    @NotBlank(message = "请选择所在城市")
    private String district;
}

激活用户信息入库

service-user  com/imooc/user/controller/UserController.java
@Override
    public GraceJSONResult updateUserInfo(UpdateUserInfoBO updateUserInfoBO, BindingResult result) {
        // 0.校验BO
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        // 1.执行更新操作
        userService.updateUserInfo(updateUserInfoBO);
        return GraceJSONResult.ok();
        //调用UserService把独有信息传入
    }
service-user  com/imooc/user/service/impl/UserService.java
package com.imooc.user.service.impl;

import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;

public interface UserService {
    /**
     * 判断用户是否存在,如果存在返回user信息
     */
    public AppUser queryMobileIsExist(String mobile);

    /**
     * 创建用户,新增用户记录到数据库
     */
    public AppUser createUser(String mobile);

    /**
     * 根据用户主键id查询用户信息
     * @param userId
     * @return
     */
    public AppUser getUser(String userId);

    /**
     * 用户修改信息,完善资料,并且激活
     * @param updateUserInfoBO
     */
    public void updateUserInfo(UpdateUserInfoBO updateUserInfoBO);
}
service-user  com/imooc/user/service/UserServiceimpl.java
@Override
    public void updateUserInfo(UpdateUserInfoBO updateUserInfoBO){
        String userId = updateUserInfoBO.getId();
        AppUser userInfo = new AppUser();
        BeanUtils.copyProperties(updateUserInfoBO, userInfo);
        userInfo.setUpdatedTime(new Date());
        userInfo.setActiveStatus(UserStatus.ACTIVE.type);
        //appUserMapper.updateByPrimaryKey()//数据中现有的数据覆盖为空的
        int result = appUserMapper.updateByPrimaryKeySelective(userInfo);
        if (result != 1){
            //更新操作有问题
            GraceException.display(ResponseStatusEnum.USER_UPDATE_ERROR);
        }
    }

查询并展示用户基本信息

service-api  com/imooc/api/controller/user/UserControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
public interface UserControllerApi {

    @ApiOperation(value = "获得用户基本信息",notes = "获得用户基本信息",httpMethod = "POST")
    @PostMapping("/getUserInfo")
    public GraceJSONResult getUserInfo(@RequestParam String userId);
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,BindingResult result);
}
dev-model  com/imooc/pojo/vo/AppUserVO.java
public class AppUserVO {
    private String id;
    private String nickname;
    private String face;
    private Integer activeStatus;
}Getter+Setter
service-user  com/imooc/user/controller/UserController.java
@RestController
public class UserController extends BaseController implements UserControllerApi {
    final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    @Override
    public GraceJSONResult getUserInfo(String userId) {
        //重写接口进行解耦!!
        // 0. 判断参数不为空
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);
        }
        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        AppUserVO userVO = new AppUserVO();
        BeanUtils.copyProperties(user, userVO); //拷贝信息
        return GraceJSONResult.ok(userVO);
    }
}

浏览器存储介质

  • SessionStorage
  • localStorage

缓存用户信息 [用Redis减轻数据库压力]

service-api  com/imooc/api/BaseController.java
//REDIS_USER_INFO添加进来
public abstract class BaseController {
    @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写
    public static final String REDIS_USER_INFO = "redis_user_info";//ctrl+shift+u直接大写
    public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;
    @Value("${website.domain-name}")
    public String DOMAIN_NAME;

    /**
     * 可以公用 就放到BaseController里面
     * 在任何controller中都可以调用和使用
     * 获取BO中的错误信息
     *
     * @param result
     * @return
     */
    public Map<String, String> getErrors(BindingResult result) {
        //对应着RegistLoginBO的信息
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList) {
            //发生验证错误所对应的某个属性
            String field = error.getField();
            //验证的错误信息
            String msg = error.getDefaultMessage();
            map.put(field, msg);
        }
        return map;
    }

    public void setCookie(HttpServletRequest request,
                          HttpServletResponse response,
                          String cookieName,
                          String cookieValue,
                          Integer maxAge) {
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8");
//            Cookie cookie = new Cookie(cookieName,cookieValue);
//            cookie.setMaxAge(maxAge);
//            cookie.setDomain("imoocnews.com");
//            cookie.setPath("/");//都用cookie
            setCookieValue(request, response, cookieName, cookieValue, maxAge);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public void setCookieValue(HttpServletRequest request,
                               HttpServletResponse response,
                               String cookieName,
                               String cookieValue,
                               Integer maxAge) {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setMaxAge(maxAge);
//        cookie.setDomain("imoocnews.com");
        cookie.setDomain(DOMAIN_NAME);
        cookie.setPath("/");//都用cookie
        response.addCookie(cookie);//把cookie传入
    }
service-user  com/imooc/user/controller/UserController.java
 private AppUser getUser(String userId){
        //查询判断redis中是否包含用户信息 若有则直接返回就不去查询数据库了
        String userJson = redis.get(REDIS_USER_INFO + ":" + userId);
        AppUser user = null;
        if (StringUtils.isNotBlank(userJson)){
            //字符串转换成json对象  要提取user 所以要一开始赋值null
            user = JsonUtils.jsonToPojo(userJson, AppUser.class);
        } else {
            // TODO 本方法后续公用,并且扩展
            user = userService.getUser(userId);
            // 由于用户信息不怎么会变动,对于一些千万级别网站来说,这类信息不会直接去查询数据库
            // 可以完全依靠Redis,直接把查询后的数据存入到Redis中
            // set里面设置一个key去BaseController里设置  ↓user变成jason转换类
            redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));
        }

        return user;
    }
service-user  com/imooc/user/service/UserServiceimpl.java
package com.imooc.user.service;

import com.imooc.enums.Sex;
import com.imooc.enums.UserStatus;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.service.impl.UserService;
import com.imooc.utils.DesensitizationUtil;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.RedisOperator;
import org.n3r.idworker.Sid;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
import com.imooc.utils.DateUtil;

import java.util.Date;

@Service
public class UserServiceimpl implements UserService {
    @Autowired
    public AppUserMapper appUserMapper; //基本的CRUD都可以

    @Autowired
    public Sid sid;
    public static final String REDIS_USER_INFO = "redis_user_info";//ctrl+shift+u直接大写


    @Autowired
    public RedisOperator redis;

    private static final String USER_FACE0 = "https://raw.githubusercontent.com/P-luminary/images/10d94134b65e13cc8ec9b8a9aeae4f958921cab7/data/Imooc_Cat.jpg";
    private static final String USER_FACE1 = "https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg";
    @Override
    public AppUser queryMobileIsExist(String mobile) {
        Example userExample = new Example(AppUser.class);
        Example.Criteria userCriteria = userExample.createCriteria();
        userCriteria.andEqualTo("mobile", mobile);
        AppUser user = appUserMapper.selectOneByExample(userExample);
        return user;
    }

    @Transactional //对整个类的方法,事务起作用。无异常时正常提交,有异常时数据回滚
    @Override
    public AppUser createUser(String mobile) {
        /**
         * 互联网项目都要考虑可扩展性
         * 如果未来的业务激增,那么就需要分表分库
         * 那么数据库表主键id必须保证全局(全库)唯一,不得重复
         */
        String userId = sid.nextShort();
        AppUser user = new AppUser();
        user.setId(userId);
        user.setMobile(mobile);
        user.setNickname("用户:" + DesensitizationUtil.commonDisplay(mobile)); //給手机号加** 是脱敏操作
        user.setFace(USER_FACE1);
        user.setBirthday(DateUtil.stringToDate("2024-06-29")); //字符串转换Date类型
        user.setSex(Sex.secret.type);
        user.setActiveStatus(UserStatus.INACTIVE.type);//是否激活
        user.setTotalIncome(0);//收入
        user.setCreatedTime(new Date());
        user.setUpdatedTime(new Date());
        appUserMapper.insert(user);
        return user;
    }

    @Override
    public AppUser getUser(String userId) {
        return appUserMapper.selectByPrimaryKey(userId);
    }

    @Override
    public void updateUserInfo(UpdateUserInfoBO updateUserInfoBO){
        String userId = updateUserInfoBO.getId();
        AppUser userInfo = new AppUser();
        BeanUtils.copyProperties(updateUserInfoBO, userInfo);

        userInfo.setUpdatedTime(new Date());
        userInfo.setActiveStatus(UserStatus.ACTIVE.type);
        //appUserMapper.updateByPrimaryKey()//数据中现有的数据覆盖为空的
        int result = appUserMapper.updateByPrimaryKeySelective(userInfo);
        if (result != 1){
            //更新操作有问题
            GraceException.display(ResponseStatusEnum.USER_UPDATE_ERROR);
        }
        // 再次查询用户的最新信息,放入redis中
        AppUser user = getUser(userId);
        redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));

    }
}
Redis里面 redis_user_info
{"id":"240629F21AK1BHX4","mobile":"15027597319","nickname":"15027597319","face":"https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg","realname":"小宝宝的小潘潘2","email":"390415030@qq.com","sex":1,"birthday":1720195200000,"province":"河北","city":"唐山市","district":"丰润区","activeStatus":1,"totalIncome":0,"createdTime":1719661387000,"updatedTime":1720281759000}

双写数据不一致的情况 [redis故障没有写入新数据]

如何双写一致 缓存双删

用户先把老Redis中的数据删除 然后再把修改值放入数据库 然后数据库再导入redis 就可以保证双写一致
但是要保证数据库放入Redis之前 后期用户请求要再其之后 [进行休眠] =>缓存双删

service-user  com/imooc/user/service/UserServiceimpl.java  @Override
    public void updateUserInfo(UpdateUserInfoBO updateUserInfoBO){
        String userId = updateUserInfoBO.getId();
        // 保证双写一致,先删除redis中的数据,后更新数据库
//        redis.del(REDIS_USER_INFO + ":" + userId);

        AppUser userInfo = new AppUser();
        BeanUtils.copyProperties(updateUserInfoBO, userInfo);

        userInfo.setUpdatedTime(new Date());
        userInfo.setActiveStatus(UserStatus.ACTIVE.type);
        //appUserMapper.updateByPrimaryKey()//数据中现有的数据覆盖为空的
        int result = appUserMapper.updateByPrimaryKeySelective(userInfo);
        if (result != 1){
            //更新操作有问题
            GraceException.display(ResponseStatusEnum.USER_UPDATE_ERROR);
        }
        // 再次查询用户的最新信息,放入redis中
        AppUser user = getUser(userId);
        redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));

        // 缓存双删策略 [不处理可能会缓存击穿]
        try {
            Thread.sleep(100);
            redis.del(REDIS_USER_INFO + ":" + userId);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

CAP理论 [只能满足其中一、二]

分布式系统都存在CAP情况

定理

CAP的重要性

分布式不可能同时满足三个条件 【先满足P再去考虑A或C】

CAP理论是什么?
  • C(Consistency, 一致性):读操作是否总能读到前一个写操作的结果 [某节点获得的数据都是一样的] 在本项目中一致性位于Session Storage
  • A(Availability, 可用性):非故障节点应该在合理的时间内作出合理的响应(不是错误或超时的响应),但是可能不是最新的数据。 [某个挂掉了 其他还可以用]
  • P(Partition tolerance, 分区容错):当出现网络分区现象后,系统能够继续运行。分区容错性

CAP如何选择?
  • CP[支付宝]或者AP[超级跑跑系统维护]
  • 在什么场合,可用性高于一致性?
    • 网页必须要保障可用性(一定能看到最重要 是不是最新的不重要)和分区容错
    • 支付的时候一定要保障一致性(我可以保证不可用 但我不允许余额不一致)和分区容错
  • 合适的才是最好的
  • CP:Redis【保证数据一致性 一定要满足C】
  • AP:会采用弱一致性 淘宝下单只需要知道下单就好 数量一致性商家可以慢慢调整
  • CA:单体存在架构、关系型架构

在本项目中如果采用弱一致性:可以不把用户存到session Storage 直接显示

集群、分布式、微服务的区别

集群和分布式的区别
  • 分布式:一个业务分拆多个子业务,部署在不同的服务器上 [服务器之间要通信]
  • 集群:同一个业务,部署在多个服务器上 [五台机器可以不通信]
集群和微服务的区别
  • 集群:分散压力
  • 微服务:分散压力
微服务和分布式的区别
  • 微服务是架构设计方式 [逻辑架构]

  • 分布式是系统部署方式 [物理架构]

  • 微服务:是一种架构方式 [大的服务拆成小的服务 每个服务独立开发测试]

  • 分布式:主要强调部署的方式

用户会话拦截器 [必须用户登陆后才可以用其他界面]

service-api  com/imooc/api/interceptors/UserTokenInterceptor.java
package com.imooc.api.interceptors;

import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserTokenInterceptor extends BaseInterceptor implements HandlerInterceptor {

    /**
     * 拦截请求,访问controller之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 有些接口同时会給安卓 H5等 所以不去cookie拿
        String userId = request.getHeader("headerUserId");
        String userToken = request.getHeader("headerUserToken");

        // 判断是否放行
        boolean run = verifyUserIdToken(userId, userToken, REDIS_USER_TOKEN);

        /**
         * false:请求被拦截
         * true:请求通过验证,放行
         */
        return true;
    }


    /**
     * 请求访问到controller之后,渲染视图之前
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     * 请求访问到controller之后,渲染视图之后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
package com.imooc.api.interceptors;

import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

public class BaseInterceptor {
    @Autowired
    private RedisOperator redis;
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写

    public boolean verifyUserIdToken(String id,
                                     String token,
                                     String redisKeyPrefix){ //redis..前缀
        if (StringUtils.isNotBlank(id) && StringUtils.isNotBlank(token)){
            String redisToken = redis.get(redisKeyPrefix + ":" + id);
            if (StringUtils.isBlank(id)){
                GraceException.display(ResponseStatusEnum.UN_LOGIN);
                return false;
            } else {
                if (!redisToken.equalsIgnoreCase(token)){//是否和传入token一致
                    GraceException.display(ResponseStatusEnum.TICKET_INVALID);
                    return false;
                }
            }
        }else {
            GraceException.display(ResponseStatusEnum.UN_LOGIN);
            return false;
        }
        return true;
    }
}
service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId");
    }
}
/*
在你的 Spring 应用程序中,InterceptorConfig 类中的 @Bean 注解用于定义 PassportInterceptor 和 UserTokenInterceptor 的 bean。这使这些拦截器对象在 Spring 上下文中可用,以便进行依赖注入。

删除 @Bean 注解后会出现错误 Autowired members must be defined in valid Spring bean (@Component|@Service|...),这是因为 BaseInterceptor 类中有一个需要由 Spring 注入的依赖 (RedisOperator redis)。要让 Spring 执行依赖注入,包含 @Autowired 注解的类必须是一个由 Spring 管理的 bean,可以通过 @Component、@Service、@Controller 等注解或在配置类中通过 @Bean 来定义。

这里是对 @Bean 的作用以及为什么删除它会导致错误的详细解释:

使用 @Bean 定义 Bean:
在 InterceptorConfig 类中,@Bean 注解定义了 PassportInterceptor 和 UserTokenInterceptor 作为 Spring 的 bean。这使得它们在整个应用程序中可用于依赖注入。

依赖注入的要求:
BaseInterceptor 类中使用了 @Autowired 注解来注入 RedisOperator。要使这个注入有效,BaseInterceptor 必须是一个 Spring 管理的 bean。而 @Bean 注解在配置类中定义了这些拦截器,使得 Spring 可以管理它们,并在需要时进行依赖注入。

如果删除了 @Bean 注解,PassportInterceptor 和 UserTokenInterceptor 将不再是 Spring 管理的 bean,从而导致在它们内部或相关联的类(如 BaseInterceptor)中的依赖无法被注入。这就是为什么删除 @Bean 注解后会出现 Autowired members must be defined in valid Spring bean (@Component|@Service|...) 错误的原因。
*/

用户状态激活拦截器

service-api  com/imooc/api/interceptors/UseActiveInterceptor.java
package com.imooc.api.interceptors;

import com.imooc.enums.UserStatus;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 用户激活状态检测拦截器
 * 发文章,修改文章,删除文章,发表评论,查看评论等
 * 这些接口都是要在用户激活后才能进行操作
 * 否则需要提示用户前往[账号设置]去修改信息
 */
    public class UseActiveInterceptor extends BaseInterceptor implements HandlerInterceptor {

    /**
     * 拦截请求,访问controller之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 有些接口同时会給安卓 H5等 所以不去cookie拿
        String userId = request.getHeader("headerUserId");
        String userJson = redis.get(REDIS_USER_INFO + ":" + userId);
        AppUser user = null;
        if (StringUtils.isNotBlank(userJson)){
            user = JsonUtils.jsonToPojo(userJson, AppUser.class);
        } else {
            GraceException.display(ResponseStatusEnum.UN_LOGIN);
        }
        if (user.getActiveStatus() == null || user.getActiveStatus() != UserStatus.ACTIVE.type){
            GraceException.display(ResponseStatusEnum.USER_INACTIVE_ERROR);
            return false;
            //随后去拦截器里进行@Bean注册 [下下个代码就是]
        }

        /**
         * false:请求被拦截
         * true:请求通过验证,放行
         */
        return true;
    }

    /**
     * 请求访问到controller之后,渲染视图之前
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }
    /**
     * 请求访问到controller之后,渲染视图之后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
service-user  com/imooc/user/controller/PassportController.java
//[加一行redis.set(REDIS_USER_INFO+":"+user.getId(), JsonUtils.objectToJson(user));]
@Override
    public GraceJSONResult doLogin(RegistLoginBO registLoginBO, BindingResult result, HttpServletRequest request, HttpServletResponse response) {
        //0.判断BindingResult中是否保存了错误的验证信息 如果有则需要返回
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }
        String mobile = registLoginBO.getMobile();
        String smsCode = registLoginBO.getSmsCode();

        //1.校验验证码是否匹配[在redis中去获取]
        String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile); //为空||不同值
        if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);
        }

        //2.查询数据库,判断该用户注册
        AppUser user = userService.queryMobileIsExist(mobile);
        if (user != null && user.getActiveStatus() == UserStatus.FROZEN.type){
            //如果用户不为空,并且状态为冻结,则直接抛出异常,禁止登录
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);
        }else if (user == null){
            //如果用户没有注册过,则为null,需要注册信息入库
            user = userService.createUser(mobile);
        }

        // 3.保存用户分布式会话的相关操作
        int userActiveStatus = user.getActiveStatus();
        if (userActiveStatus != UserStatus.FROZEN.type){
            String uToken = UUID.randomUUID().toString();
            redis.set(REDIS_USER_TOKEN+":"+user.getId(),uToken);//BaseController里面 保存token到redis
            redis.set(REDIS_USER_INFO+":"+user.getId(), JsonUtils.objectToJson(user));

            //保存用户id和token到cookie中 设计一个request response 回到PassportControllerApi
            setCookie(request, response,"utoken",uToken,COOKIE_MONTH);
            setCookie(request, response,"uid",user.getId(),COOKIE_MONTH);
        }
        // 4.用户登录或注册成功以后,需要删除redis中的短信验证码,验证码只能使用一次,用过则作废
//        redis.del(MOBILE_SMSCODE + ":" + mobile);
        // 5.返回用户状态 返回前端看
        return GraceJSONResult.ok(userActiveStatus);
    }
service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UseActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UseActiveInterceptor useActiveInterceptor(){
        return new UseActiveInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId");
//        registry.addInterceptor(userTokenInterceptor())
//                .addPathPatterns("/user/getAccountInfo")
    }
}

AOP警告日志监控与sql打印 [切面AOP通知编程]

dev-common 引入aop依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
service-api  com/imooc/api/aspect/ServiceLogAspect.java
package com.imooc.api.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ServiceLogAspect {
    final static Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
    /**
     * AOP通知:
     * 1.前置通知
     * 2.后置通知
     * 3.环绕通知 ★★
     * 4.异常通知
     * 5.最终通知
     */
    //*是返回所有类型 匹配包的位置 *.* = 任意文件.任意后缀  (..)是任意类和任意方法
    @Around("execution(* com.imooc.*.service.impl..*.*(..))")
    public Object recordTimeOfService(ProceedingJoinPoint joinPoint) throws Throwable {
        logger.info("==== 开始执行 {}.{} ====",
                joinPoint.getTarget().getClass(),
                joinPoint.getSignature().getName());
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        long takeTime = end - start;
        if (takeTime > 3000){
            logger.error("当前执行耗时:{}",takeTime);
        }else if (takeTime > 2000){
            logger.warn("当前执行耗时:{}",takeTime);
        }else {
            logger.info("当前执行耗时:{}",takeTime);
        }
        return result;
    }
}
====================================================================
http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html
提交信息 看后台Terminal
service-user  application-dev.yml #增加一个open mybatis log in dev
  server:
  port: 8003

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
# open mybatis log in dev
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# setup CN from java, This is resource
website:
  domain-name: imoocnews.com

===================================================================
如果在未来发生sql错误可以通过sql输出来找到sql语句从而放入运行检查错误 (21 28行)
JDBC Connection [HikariProxyConnection@2054571226 wrapping org.mariadb.jdbc.MariaDbConnection@4b4b68f8] will not be managed by Spring
==>  Preparing: UPDATE app_user SET nickname = ?,face = ?,realname = ?,email = ?,sex = ?,birthday = ?,province = ?,city = ?,district = ?,active_status = ?,updated_time = ? WHERE id = ? 
==> Parameters: 15027597319(String), https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg(String), 小宝宝的小潘潘(String), 390415030@qq.com(String), 1(Integer), 2024-07-06 00:00:00.0(Timestamp), 河北(String), 唐山市(String), 丰润区(String), 1(Integer), 2024-07-07 22:41:09.862(Timestamp), 240629F21AK1BHX4(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9176eb0]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65bd9477] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1798162927 wrapping org.mariadb.jdbc.MariaDbConnection@4b4b68f8] will not be managed by Spring
==>  Preparing: SELECT id,mobile,nickname,face,realname,email,sex,birthday,province,city,district,active_status,total_income,created_time,updated_time FROM app_user WHERE id = ? 
==> Parameters: 240629F21AK1BHX4(String)
<==    Columns: id, mobile, nickname, face, realname, email, sex, birthday, province, city, district, active_status, total_income, created_time, updated_time
<==        Row: 240629F21AK1BHX4, 15027597319, 15027597319, https://raw.githubusercontent.com/P-luminary/images/875ad52658686e6cc3a8e0cd75d2a324a3d742a9/data/Imooc_Girl.jpg, 小宝宝的小潘潘, 390415030@qq.com, 1, 2024-07-06, 河北, 唐山市, 丰润区, 1, 0, 2024-06-29 19:43:07.0, 2024-07-07 22:41:09.0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@65bd9477]
41:10.009 [http-nio-8003-exec-3] INFO  io.lettuce.core.EpollProvider - Starting without optional epoll library
41:10.010 [http-nio-8003-exec-3] INFO  io.lettuce.core.KqueueProvider - Starting without optional kqueue library
41:10.460 [http-nio-8003-exec-3] INFO  c.imooc.api.aspect.ServiceLogAspect - 当前执行耗时:601

退出登录、注销会话

service-api  com/imooc/api/controller/user/PassportControllerApi.java
//用户登录信息的redis和cookies清除
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid; //用户需求验证

@Api(value = "用户注册登录",tags = {"用户注册登录的Controller"})
@RequestMapping("passport")
public interface PassportControllerApi {
    @ApiOperation(value = "获得短信验证码",notes = "获得短信验证码",httpMethod = "GET")
    @GetMapping("/getSMSCode")
    public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);

    @ApiOperation(value = "一键注册登录接口",notes = "一键注册登录接口",httpMethod = "POST")
    @PostMapping("/doLogin") //表单里面用post  RequestBody后面传过来的东西和json对象对应
    public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO
            , BindingResult result, HttpServletRequest request, HttpServletResponse response);
    //完成之后 去BaseController里面写一个setCookie()方便都可以用

    @ApiOperation(value = "用户退出登录",notes = "用户退出登录",httpMethod = "POST")
    @PostMapping("/logout")
    public GraceJSONResult logout(@RequestParam String userId,
                                  HttpServletRequest request,
                                  HttpServletResponse response);
}
service-user  com/imooc/user/controller/PassportController.java
@Override
    public GraceJSONResult logout(String userId,
                                  HttpServletRequest request,
                                  HttpServletResponse response){
        redis.del(REDIS_USER_TOKEN + ":" + userId);
        //USER_INFO可以不用删 可能后面会查询 没有清除cookie只有重新设置时间为0
        setCookie(request, response, "utoken","",COOKIE_DELETE);
        setCookie(request, response, "uid","",COOKIE_DELETE);
        return GraceJSONResult.ok();
    }

FastDFS架构原理与上传下载流程解析

文件服务器
  • 传统服务器存储

  • FastDFS
  • OSS

  • GridFS

配置FastDFS环境准备工作

环境准备
名称 说明
Centos 7.x
libfastcommon-1.0.42.tar.gz FastDFS分离出的一些公用函数包
FastDFS FastDFS本体
fastdfs-nginx-module-1.22.tar.gz FastDFS和nginx的关联模块
nginx nginx1.15.4
它跟我说要准备两个虚拟机[tracker 和 storage 版本都是CentOS 7.x]
//创建一个FastDFS文件夹
[imooc@imooc FastDFS]$ ll
总用量 1980
-rw-rw-r--. 1 imooc imooc  800157 7月   9 15:53 fastdfs-6.04.tar.gz
-rw-rw-r--. 1 imooc imooc   19952 7月   9 15:53 fastdfs-nginx-module-1.22.tar.gz
-rw-rw-r--. 1 imooc imooc  164704 7月   9 15:53 libfastcommon-1.0.42.tar.gz
-rw-rw-r--. 1 imooc imooc 1032630 7月   9 15:53 nginx-1.16.1.tar.gz
[imooc@imooc FastDFS]$ tar -zxvf libfastcommon-1.0.42.tar.gz 
[imooc@imooc FastDFS]$ cd libfastcommon-1.0.42/
[imooc@imooc libfastcommon-1.0.42]$ ll
总用量 32
drwxrwxr-x. 2 imooc imooc   114 12月  5 2019 doc
-rw-rw-r--. 1 imooc imooc 10054 12月  5 2019 HISTORY
-rw-rw-r--. 1 imooc imooc   674 12月  5 2019 INSTALL
-rw-rw-r--. 1 imooc imooc  1607 12月  5 2019 libfastcommon.spec
-rwxrwxr-x. 1 imooc imooc  3253 12月  5 2019 make.sh
drwxrwxr-x. 2 imooc imooc   191 12月  5 2019 php-fastcommon
-rw-rw-r--. 1 imooc imooc  2776 12月  5 2019 README
drwxrwxr-x. 3 imooc imooc  4096 12月  5 2019 src
[imooc@imooc libfastcommon-1.0.42]$ ./make.sh
[imooc@imooc libfastcommon-1.0.42]$ sudo ./make.sh install
//安装解压包的本体
[imooc@imooc FastDFS]$ tar -zxvf fastdfs-6.04.tar.gz 
[imooc@imooc FastDFS]$ cd fastdfs-6.04/
[imooc@imooc fastdfs-6.04]$ ./make.sh
[imooc@imooc fastdfs-6.04]$ sudo ./make.sh install
[imooc@imooc fastdfs-6.04]$ cd /usr/bin
[imooc@imooc bin]$ ls fdfs_*
fdfs_appender_test   fdfs_download_file        fdfs_test
fdfs_appender_test1  fdfs_file_info            fdfs_test1
fdfs_append_file     fdfs_monitor              fdfs_trackerd
fdfs_crc32           fdfs_regenerate_filename  fdfs_upload_appender
fdfs_delete_file     fdfs_storaged             fdfs_upload_file
[imooc@imooc bin]$ cd /etc/fdfs/
[imooc@imooc fdfs]$ ll
总用量 28 //这些都是配置文件 如果要修改则需要拷贝一份新鲜的
-rw-r--r--. 1 root root  1834 7月   9 16:02 client.conf.sample
-rw-r--r--. 1 root root 10085 7月   9 16:02 storage.conf.sample
-rw-r--r--. 1 root root   527 7月   9 16:02 storage_ids.conf.sample
-rw-r--r--. 1 root root  8038 7月   9 16:02 tracker.conf.sample

[imooc@imooc FastDFS]$ cd fastdfs-6.04/
[imooc@imooc fastdfs-6.04]$ cd conf/
[imooc@imooc conf]$ ll
总用量 88
-rw-rw-r--. 1 imooc imooc 23981 12月  5 2019 anti-steal.jpg
-rw-rw-r--. 1 imooc imooc  1834 12月  5 2019 client.conf
-rw-rw-r--. 1 imooc imooc   955 12月  5 2019 http.conf
-rw-rw-r--. 1 imooc imooc 31172 12月  5 2019 mime.types
-rw-rw-r--. 1 imooc imooc 10085 12月  5 2019 storage.conf
-rw-rw-r--. 1 imooc imooc   527 12月  5 2019 storage_ids.conf
-rw-rw-r--. 1 imooc imooc  8038 12月  5 2019 tracker.conf
//拷贝到etc下  安装前的准备工作
[imooc@imooc conf]$ sudo cp * /etc/fdfs/
[imooc@imooc conf]$ cd /etc/fdfs
[imooc@imooc fdfs]$ ll
总用量 116
-rw-r--r--. 1 root root 23981 7月   9 16:06 anti-steal.jpg
-rw-r--r--. 1 root root  1834 7月   9 16:06 client.conf
-rw-r--r--. 1 root root  1834 7月   9 16:02 client.conf.sample
-rw-r--r--. 1 root root   955 7月   9 16:06 http.conf
-rw-r--r--. 1 root root 31172 7月   9 16:06 mime.types
-rw-r--r--. 1 root root 10085 7月   9 16:06 storage.conf
-rw-r--r--. 1 root root 10085 7月   9 16:02 storage.conf.sample
-rw-r--r--. 1 root root   527 7月   9 16:06 storage_ids.conf
-rw-r--r--. 1 root root   527 7月   9 16:02 storage_ids.conf.sample
-rw-r--r--. 1 root root  8038 7月   9 16:06 tracker.conf
-rw-r--r--. 1 root root  8038 7月   9 16:02 tracker.conf.sample

配置tracker服务 [一个虚拟机]

//根据配置文件去区分是哪个服务
[imooc@imooc fdfs]$ cd /etc/fdfs
[imooc@imooc fdfs]$ ll
总用量 116
-rw-r--r--. 1 root root 23981 7月   9 16:06 anti-steal.jpg
-rw-r--r--. 1 root root  1834 7月   9 16:06 client.conf
-rw-r--r--. 1 root root  1834 7月   9 16:02 client.conf.sample
-rw-r--r--. 1 root root   955 7月   9 16:06 http.conf
-rw-r--r--. 1 root root 31172 7月   9 16:06 mime.types
-rw-r--r--. 1 root root 10085 7月   9 16:06 storage.conf
-rw-r--r--. 1 root root 10085 7月   9 16:02 storage.conf.sample
-rw-r--r--. 1 root root   527 7月   9 16:06 storage_ids.conf
-rw-r--r--. 1 root root   527 7月   9 16:02 storage_ids.conf.sample
-rw-r--r--. 1 root root  8038 7月   9 16:06 tracker.conf
-rw-r--r--. 1 root root  8038 7月   9 16:02 tracker.conf.sample
[imooc@imooc fdfs]$ sudo vim tracker.conf  
//里面的port=22122 bind_addr= 计算机节点 这些不动
//修改里面的base_path=/home/yuqing/fastdfs 
    //修改为→ /usr/local/fastdfs/tracker
[imooc@imooc fdfs]$ mkdir /usr/local/fastdfs/tracker -p //-p后面文件夹做递归创建
[imooc@imooc fdfs]$ sudo /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf //当成配置文件加进去 ★★★★★★★★★★★★★★★★★★
[imooc@imooc fdfs]$ ps -ef|grep tracker
root       6254      1  0 18:31 ?        00:00:00 /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf
imooc      6268   3011  0 18:31 pts/0    00:00:00 grep --color=auto tracker

配置storage服务 [另一个虚拟机]

[storage@imooc fdfs]$ cd /etc/fdfs/
[storage@imooc fdfs]$ sudo vim storage.conf
//[修改后] group_name=imooc 
//[修改后] bath_path=/usr/local/fastdfs/storage
[storage@imooc fdfs]$ sudo mkdir /usr/local/fastdfs/storage -p
[storage@imooc fdfs]$ cd /usr/local/
[storage@imooc local]$ ll
[storage@imooc local]$ cd fastdfs/
[storage@imooc fastdfs]$ ll
[storage@localhost fastdfs]$ ll
总用量 0
drwxr-xr-x. 2 root root 6 7月   9 18:38 storage
[storage@localhost fastdfs]$ cd /etc/fdfs/  //接着修改storage
[storage@imooc fdfs]$ sudo vim storage.conf
//[修改后] store_path0=/usr/local/fastdfs/storage
//配置到tracker的ip地址[修改后] tracker_server=192.168.170.135:22122
/ ‘/8888’  http.server_port=8888 是web的相关端口号
[storage@localhost fdfs]$ sudo /usr/bin/fdfs_storaged /etc/fdfs/storage.conf //★★★★
★一定要先启动tracker 再去启动storage 不然service发不过去★
//配置客户端做上传动作
[imooc@imooc ~]$ cd /etc/fdfs
[storage@localhost fdfs]$ pwd
/etc/fdfs
[storage@localhost fdfs]$ sudo vim client.conf
//[修改后]base_path=/usr/local/fastdfs/client
[storage@localhost fdfs]$ sudo mkdir /usr/local/fastdfs/client
[storage@localhost fdfs]$ cd /usr/local/fastdfs
[storage@localhost fastdfs]$ ll
总用量 0
drwxr-xr-x. 2 root root  6 7月   9 19:26 client
drwxr-xr-x. 4 root root 30 7月   9 19:04 storage
[storage@localhost fastdfs]$ cd /etc/fdfs/
[storage@localhost fdfs]$ sudo vim client.conf
//[修改后]tracker_server=192.168.170.135:22122
[storage@localhost fdfs]$ cd /usr/bin
[storage@localhost bin]$ ls fdfs*
fdfs_appender_test   fdfs_download_file        fdfs_test
fdfs_appender_test1  fdfs_file_info            fdfs_test1
fdfs_append_file     fdfs_monitor              fdfs_trackerd
fdfs_crc32           fdfs_regenerate_filename  fdfs_upload_appender
fdfs_delete_file     fdfs_storaged             fdfs_upload_file
//fdfs_test在命令行去测试
[storage@localhost bin]$ cd /home/
[storage@localhost home]$ cd /usr/local/fastdfs/storage/
[storage@localhost storage]$ cd data
[storage@localhost data]$ cd 00
[storage@localhost data]$ ll   【里面很多十六进制数据】
[storage@localhost data]$ cd 00 
[storage@localhost data]$ ll   //【里面没有数据 上传图片到这里查看是否成功】
///home/storage 这里有一张测试图片log.png [自行添加]
[storage@localhost 00]$ pwd
/usr/local/fastdfs/storage/data/00/00
[storage@localhost 00]$ cd /etc/fdfs
[storage@localhost ~]$ cd /etc/fdfs/
[storage@localhost fdfs]$ cd /usr/bin/
[storage@localhost bin]$ ls fdfs*
fdfs_appender_test   fdfs_download_file      //fdfs_test
fdfs_appender_test1  fdfs_file_info            fdfs_test1
fdfs_append_file     fdfs_monitor              fdfs_trackerd
fdfs_crc32           fdfs_regenerate_filename  fdfs_upload_appender
fdfs_delete_file     fdfs_storaged             fdfs_upload_file
[storage@localhost bin]$ ./fdfs_test /etc/fdfs/client.conf upload /home/storage/log.png 
/*
This is FastDFS client test program v6.04

Copyright (C) 2008, Happy Fish / YuQing

FastDFS may be copied only under the terms of the GNU General
Public License V3, which may be found in the FastDFS source kit.
Please visit the FastDFS Home Page http://www.fastken.com/ 
for more detail.

[2024-07-09 19:39:25] DEBUG - base_path=/usr/local/fastdfs/client, connect_timeout=10, network_timeout=60, tracker_server_count=1, anti_steal_token=0, anti_steal_secret_key length=0, use_connection_pool=0, g_connection_pool_max_idle_time=3600s, use_storage_id=0, storage server id count: 0

tracker_query_storage_store_list_without_group: 
    server 1. group_name=, ip_addr=192.168.170.136, port=23000

group_name=imooc【企业简写】, ip_addr=192.168.170.136, port=23000
storage_upload_by_filename
group_name=imooc, remote_filename=M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png 
【remote_filename:重组路径】【因为还没有发布文件服务 所以无法直接查看文件】
source ip address: 192.168.170.136
file timestamp=2024-07-09 19:39:25
file size=12618
file crc32=630904148
example file url: http://192.168.170.136/imooc/M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png
storage_upload_slave_by_filename
group_name=imooc, remote_filename=M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png
source ip address: 192.168.170.136
file timestamp=2024-07-09 19:39:25
file size=12618
file crc32=630904148
example file url: http://192.168.170.136/imooc/M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png
 */
[storage@localhost bin]$ cd /usr/local/fastdfs/storage/data/
[storage@localhost data]$ cd 00
[storage@localhost 00]$ cd 00
[storage@localhost 00]$ ll
总用量 40
-rw-r--r--. 1 root root 12618 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png
-rw-r--r--. 1 root root    49 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png-m
-rw-r--r--. 1 root root 12618 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png
-rw-r--r--. 1 root root    49 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png-m

安装Nginx提供Web服务 [通过浏览器访问到文件]

Nginx是反向代理服务器可以做集群 也可以控制多个虚拟主机
-rw-rw-r--. 1 storage storage 142245547 7月  10 15:32 jdk-7u75-linux-x64.tar.gz
-rw-rw-r--. 1 storage storage   1032630 7月  10 15:33 nginx-1.16.1.tar.gz
//[storage@localhost ~]$ sudo yum install gcc-c++
已加载插件:fastestmirror, langpacks
Determining fastest mirrors
//[storage@localhost ~]$ sudo yum install -y pcre pcre-devel
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
//[storage@localhost ~]$ sudo yum install -y zlib zlib-devel
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
//[storage@localhost ~]$ sudo yum install -y openssl openssl-devel
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
//[storage@localhost ~]$ tar -zxvf nginx-1.16.1.tar.gz
nginx-1.16.1.tar.gz
//[storage@localhost ~]$ cd nginx-1.16.1.tar.gz
[storage@localhost nginx-1.16.1]$ sudo mkdir /var/temp/nginx -p
//创建所需的临时目录:
sudo mkdir -p /var/temp/nginx/client
sudo mkdir -p /var/temp/nginx/proxy
sudo mkdir -p /var/temp/nginx/fastcgi
sudo mkdir -p /var/temp/nginx/uwsgi
sudo mkdir -p /var/temp/nginx/scgi
[storage@localhost nginx-1.16.1]$ ./configure \  //【预配置】
> --prefix=/usr/local/nginx \
> --pid-path=/var/run/nginx/nginx.pid \
> --lock-path=/var/lock/nginx.lock \
> --error-log-path=/var/log/nginx/error.log \
> --http-log-path=/var/log/nginx/access.log \
> --with-http_gzip_static_module \
> --http-client-body-temp-path=/var/temp/nginx/client \
> --http-proxy-temp-path=/var/temp/nginx/proxy \
> --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
> --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
> --http-scgi-temp-path=/var/temp/nginx/scgi
[storage@localhost nginx-1.16.1]$ make  //【编译】
/* linux中的网络不可达
如果镜像出了问题 一定要换一下镜像配置
1. 编辑 CentOS 的 YUM 配置文件:
编辑 /etc/yum.repos.d/CentOS-Base.repo 文件:

复制代码
sudo vi /etc/yum.repos.d/CentOS-Base.repo

2. 使用以下内容更新 CentOS-Base.repo 文件:
复制代码
[base]
name=CentOS-$releasever - Base
baseurl=http://vault.centos.org/7.9.2009/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

[updates]
name=CentOS-$releasever - Updates
baseurl=http://vault.centos.org/7.9.2009/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

[extras]
name=CentOS-$releasever - Extras
baseurl=http://vault.centos.org/7.9.2009/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
*/
[storage@localhost nginx-1.16.1]$ sudo make install
[storage@localhost nginx-1.16.1]$ cd /usr/local
[storage@localhost local]$ ll
总用量 0
drwxr-xr-x. 2 root root  6 4月  11 2018 bin
drwxr-xr-x. 2 root root  6 4月  11 2018 etc
drwxr-xr-x. 4 root root 35 7月   9 19:26 fastdfs
drwxr-xr-x. 2 root root  6 4月  11 2018 games
drwxr-xr-x. 2 root root  6 4月  11 2018 include
drwxr-xr-x. 2 root root  6 4月  11 2018 lib
drwxr-xr-x. 2 root root  6 4月  11 2018 lib64
drwxr-xr-x. 2 root root  6 4月  11 2018 libexec
drwxr-xr-x. 5 root root 42 7月  10 16:55 nginx
drwxr-xr-x. 2 root root  6 4月  11 2018 sbin
drwxr-xr-x. 5 root root 49 7月   9 17:16 share
drwxr-xr-x. 2 root root  6 4月  11 2018 src
[storage@localhost local]$ cd nginx/
[storage@localhost nginx]$ ll
总用量 4
drwxr-xr-x. 2 root root 4096 7月  10 16:55 conf
drwxr-xr-x. 2 root root   40 7月  10 16:55 html
drwxr-xr-x. 2 root root   19 7月  10 16:55 sbin
[storage@localhost nginx]$ cd sbin/
[storage@localhost sbin]$ ll
总用量 3768
-rwxr-xr-x. 1 root root 3857144 7月  10 16:55 nginx
[storage@localhost sbin]$ sudo ./nginx
[storage@localhost sbin]$ ps -ef|grep nginx
root       6642      1  0 16:58 ?        00:00:00 nginx: master process ./nginx
nobody     6643   6642  0 16:58 ?        00:00:00 nginx: worker process
storage    6651   2975  0 16:58 pts/0    00:00:00 grep --color=auto nginx

// 在浏览器输入:http://192.168.170.136/ 【如果没显示应该是虚拟机的防火墙拦截 可以禁止防火墙】
Welcome to nginx!
If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

[storage@localhost nginx]$ cd html
[storage@localhost html]$ ll
总用量 8
-rw-r--r--. 1 root root 494 7月  10 16:55 50x.html
-rw-r--r--. 1 root root 612 7月  10 16:55 index.html

[storage@localhost html]$ sudo ../sbin/nginx -t //【测试刚刚的步骤是否正确】
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

整合Nginx实现文件服务器

[storage@localhost FastDFS]$ tar -zxvf fastdfs-nginx-module-1.22.tar.gz 
fastdfs-nginx-module-1.22/
fastdfs-nginx-module-1.22/HISTORY
fastdfs-nginx-module-1.22/INSTALL
fastdfs-nginx-module-1.22/src/
fastdfs-nginx-module-1.22/src/common.c
fastdfs-nginx-module-1.22/src/common.h
fastdfs-nginx-module-1.22/src/config
fastdfs-nginx-module-1.22/src/mod_fastdfs.conf
fastdfs-nginx-module-1.22/src/ngx_http_fastdfs_module.c
[storage@localhost FastDFS]$ cd fastdfs-nginx-module-1.22/
[storage@localhost fastdfs-nginx-module-1.22]$ ll
总用量 8
-rw-rw-r--. 1 storage storage 3036 11月 19 2019 HISTORY
-rw-rw-r--. 1 storage storage 2001 11月 19 2019 INSTALL
drwxrwxr-x. 2 storage storage  109 11月 19 2019 src
[storage@localhost fastdfs-nginx-module-1.22]$ cd src/
[storage@localhost src]$ sudo cp mod_fastdfs.conf /etc/fdfs/
[storage@localhost ~]$ cd /etc/fdfs/
[storage@localhost fdfs]$ sudo vim mod_fastdfs.conf 
//【布置存储路径】
/*
store_path0=/usr/local/fastdfs/storage
tracker_server=192.168.170.135:22122
group_name=imooc
url_have_group_name = true
base_path=/usr/local/fastdfs/tmp
*/
[storage@localhost FastDFS]$ cd fastdfs-nginx-module-1.22/
[storage@localhost fastdfs-nginx-module-1.22]$ cd src/
[storage@localhost src]$ vim config
/local 把带有local的都删掉
[storage@localhost ~]$ cd nginx-1.16.1/
 [storage@localhost nginx-1.16.1]$ 
./configure \
> --prefix=/usr/local/nginx \
> --pid-path=/var/run/nginx/nginx.pid \
> --lock-path=/var/lock/nginx.lock \
> --error-log-path=/var/log/nginx/error.log \
> --http-log-path=/var/log/nginx/access.log \
> --with-http_gzip_static_module \
> --http-client-body-temp-path=/var/temp/nginx/client \
> --http-proxy-temp-path=/var/temp/nginx/proxy \
> --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
> --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
> --http-scgi-temp-path=/var/temp/nginx/scgi \
> --add-module=/home/storage/FastDFS/fastdfs-nginx-module-1.22/src
[storage@localhost nginx-1.16.1]$ sudo make && sudo make install
[storage@localhost nginx-1.16.1]$ cd /usr/local/nginx/
[storage@localhost nginx]$ cd conf/
[storage@localhost conf]$ sudo vim nginx.conf
/*
server {
        listen       8888;
        server_name  localhost;
        location ~/group[0-9]/ {
        ngx_fastdfs_module;
        }
        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

*/
[storage@localhost conf]$ vim /etc/fdfs/tracker.conf
/http.service_port:8080
[storage@localhost conf]$ sudo vim nginx.conf //★★★★★★
/*
server {
        listen       8888;
        server_name  localhost;
        location /imooc/M00 {
            ngx_fastdfs_module;
        }
        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }
}
*/
[storage@localhost conf]$ sudo ../sbin/nginx -t //测试一下有无问题
ngx_http_fastdfs_set pid=6143
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
[storage@localhost conf]$ sudo ../sbin/nginx -s reload //重新加载
ngx_http_fastdfs_set pid=6436

[storage@localhost conf]$ cd /usr/local/fastdfs/storage/
[storage@localhost storage]$ cd data
[storage@localhost data]$ cd 00/00
总用量 40
-rw-r--r--. 1 root root 12618 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png
-rw-r--r--. 1 root root    49 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457_big.png-m
-rw-r--r--. 1 root root 12618 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png
-rw-r--r--. 1 root root    49 7月   9 19:39 wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png-m
http://192.168.170.136:8888/imooc/M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png


//查看错误日志 ★★★【sudo tail -n 50 /var/log/nginx/error.log】★★★
/*
2. 重新加载 systemd 并启动 Nginx
重新加载 systemd:
复制代码
sudo systemctl daemon-reload
启动 Nginx:

sh
复制代码
sudo systemctl start nginx
设置开机自启动:

sh
复制代码
sudo systemctl enable nginx
检查 Nginx 服务状态:

sh
复制代码
sudo systemctl status nginx
*/
[storage@localhost conf]$ sudo vim /etc/fdfs/tracker.conf
[storage@localhost conf]$ sudo vim /etc/fdfs/storage.conf

[storage@localhost conf]$ cd /usr/local/nginx/conf/
[storage@localhost conf]$ sudo ../sbin/nginx -s stop
ngx_http_fastdfs_set pid=12586
[storage@localhost conf]$ sudo ../sbin/nginx
ngx_http_fastdfs_set pid=12605
[storage@localhost conf]$ sudo ../sbin/nginx -s reload

/*
FastDFS输出报告位置:
sudo tail -n 50 /usr/local/fastdfs/storage/logs/storaged.log


启动 Tracker 服务器:
sudo systemctl start fdfs_trackerd
检查 Tracker 服务器状态:
sudo systemctl status fdfs_trackerd
确认 Tracker 服务器监听端口:
sudo netstat -tuln | grep :22122
*/
//草!好几个小时的含泪史 一定要先开tracker端!!!
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★
★★★★★一定要先启动tracker 再去启动storage 不然service发不过去★★★★★

/*
首先,重新启动 FastDFS 的 tracker 和 storage 服务:

bash
复制代码
# 重启 tracker 服务
sudo systemctl restart fdfs_trackerd

# 重启 storage 服务
sudo systemctl restart fdfs_storaged
2. 重启 Nginx 服务
接下来,重新启动 Nginx 服务,确保它能够加载新的配置并生效:

bash
复制代码
sudo systemctl restart nginx
3. 验证服务状态
重新启动服务后,可以通过以下方式验证它们的运行状态:

检查 FastDFS 服务状态:

bash
复制代码
sudo systemctl status fdfs_trackerd
sudo systemctl status fdfs_storaged
检查 Nginx 服务状态:

bash
复制代码
sudo systemctl status nginx
*/

http://192.168.170.136:8888/imooc/M00/00/00/wKiqiGaNIW2AMDeaAAAxSiWa1VQ457.png

创建文件服务module [文件上传]

【新建一个module imooc-news-dev-service-files】
pom.xml
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-files</artifactId>
<!--
    imooc-news-dev-service-files
    文件服务,文件相关的操作都在此文件中进行
    文件上传   文件下载
    fastdfs  oss  gridfs
-->
    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- 文件上传fdfs工具包 -->
        <dependency>
            <groupId>com.github.tobato</groupId>
            <artifactId>fastdfs-client</artifactId>
            <version>1.27.2</version>
        </dependency>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>



    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>
application.yuml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-file
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  servlet:
    multipart:
      max-file-size: 512000 #请求文件大小限制为500kb
      max-request-size: 512000
application-dev.yuml
server:
  port: 8004

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
service-files  com/imooc/files/controller/HelloController.java
package com.imooc.files.controller;


import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    public Object hello(){
        return GraceJSONResult.ok("Hello World!");
    }
}




service-files  com/imooc/files/Application.java
package com.imooc.files;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //排除数据源
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
-----------------------------------------------------------------------
http://localhost:8004/hello
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": "Hello World!"
}

整合fdfs与service实现 [文件上传]

service-api  com/imooc/files/service/impl/UploaderServiceImpl.java
package com.imooc.files.service.impl;

import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.imooc.files.service.UploaderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Service
public class UploaderServiceImpl implements UploaderService {
    //注入客户端
    @Autowired
    public FastFileStorageClient fastFileStorageClient;

    @Override
    public String uploadFdfs(MultipartFile file, String fileExtName) throws IOException {
        StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileExtName, null);
        return storePath.getFullPath();
    }
}
service-api  com/imooc/files/service/UploaderService.java
package com.imooc.files.service;

import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

public interface UploaderService {
    public String uploadFdfs(MultipartFile file, String fileExtName) throws IOException;
}
application.yml
############################################################
#
# 用户微服务
# web访问端口号  约定:8003
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-file
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  servlet:
    multipart:
      max-file-size: 512000 #请求文件大小限制为500kb
      max-request-size: 512000

############################################################
#
# fdfs配置信息
#
############################################################
fdfs:
  connect-timeout: 30
  so-timeout: 30
  tracker-list: 192.168.170.135:22122

实现fastdfs图片存储 [文件上传]

service-api  com/imooc/api/controller/files/FileUploadControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploadControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;
}
service-files  com/imooc/files/controller/FileUploadController.java
package com.imooc.files.controller;


import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // 执行上传
                path = uploaderService.uploadFdfs(file, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);
        return GraceJSONResult.ok(path);
    }
}
//此时去上传图片会报错 报跨域异常错误
//需要在用户service-user里的Application 
//@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
//贴到com/imooc/files/Application.java

此时再次 http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html
提交头像
Console:
06:09.827 [http-nio-8004-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
06:09.827 [http-nio-8004-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
06:09.832 [http-nio-8004-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 5 ms
06:09.882 [http-nio-8004-exec-2] INFO  c.imooc.api.aspect.ServiceLogAspect - ==== 开始执行 class com.imooc.files.service.impl.UploaderServiceImpl.uploadFdfs ====
06:09.937 [http-nio-8004-exec-2] INFO  c.imooc.api.aspect.ServiceLogAspect - 当前执行耗时:55
06:09.937 [http-nio-8004-exec-2] INFO  c.i.f.c.FileUploadController - path = imooc/M00/00/00/wKiqiGaPrpKAEt22AAAeb3kUsrg507.png

http://192.168.170.136:8888/imooc/M00/00/00/wKiqiGaPrpKAEt22AAAeb3kUsrg507.png
此时就可以看到Cat的图片了!

完善用户头像上传

【在用户返回的时候写死路径+path】
return GraceJSONResult.ok("http://192.168.170.136:8888/"path);

给它包装一下 FileResource写一下
service-files  com/imooc/files/controller/FileUploadController.java
package com.imooc.files.controller;


import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.files.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Autowired
    private FileResource fileResource;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // 执行上传
                path = uploaderService.uploadFdfs(file, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);

        String finalPath = "";
        if (StringUtils.isNotBlank(path)){
            finalPath = fileResource.getHost() + path;
        }  else{
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);
        }
        return GraceJSONResult.ok(finalPath);
    }
}
service-files  com/imooc/files/FileResource.java
package com.imooc.files;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:file-${spring.profiles.active}.properties ") //这个是在application.yml里面的 自动匹配
@ConfigurationProperties(prefix = "file")
public class FileResource {
    private String host;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }
}
file-dev.properties
# fastdfs storage 节点地址(nginx整合的web服务)
file.host=http://192.168.170.136:8888/


application.yml
############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-file
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  servlet:
    multipart:
      max-file-size: 512000 #请求文件大小限制为500kb
      max-request-size: 512000
service-api  com/imooc/api/config/InterceptorConfig.java
此时拦截器也要加一层
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UseActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UseActiveInterceptor useActiveInterceptor(){
        return new UseActiveInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace");
//        registry.addInterceptor(userTokenInterceptor())
//                .addPathPatterns("/user/getAccountInfo")
    }
}

图片大小控制的统一异常处理

dev-common  com/imooc/exception/GraceExceptionHandler.java
package com.imooc.exception;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

/**
 * 统一异常拦截处理
 * 可以针对异常的类型进行捕获 然后返回json信息到前端
 */
@ControllerAdvice
public class GraceExceptionHandler {
    @ExceptionHandler(MyCustomException.class)
    //只要是这个类的异常都会进入下面的方法
    @ResponseBody
    public GraceJSONResult returnMyException(MyCustomException e){
        e.printStackTrace(); //打印信息
        return GraceJSONResult.exception(e.getResponseStatusEnum());
    }

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    @ResponseBody
    public GraceJSONResult returnMaxUploadSizeExceededException(MaxUploadSizeExceededException e){
        e.printStackTrace(); //打印信息
        return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_MAX_SIZE_ERROR);
    }
}

第三方云存储解决方案 【阿里OSS】

FastDFS [中小型公司使用]
  • 水平扩容
  • 运维复杂
  • 开发复杂【增加图片效果 & 人脸识别等】
云存储阿里OSS
  • SDK使用简单 [Java对接]
  • 提供强大的文件处理功能
  • 零运维成本
  • 图形化管理控制台
  • CDN加速
  • 降低风险管理成本

对象存储 OSS 资源包 (aliyun.com)[购买 标准-本地冗余存储下行流量 ]

控制台的基本配置使用 【阿里OSS】

OSS管理控制台 (aliyun.com)

费用与成本 (aliyun.com)我的试用

  • 对象存储OSS → Bucket列表 → 创建Bucket → 存储冗余类型:本地冗余存储 → 读写权限:公共读

OSS管理控制台 (aliyun.com)

dev-common  pom.xml
<dependency>
  <groupId>com.aliyun.oss</groupId>
  <artifactId>aliyun-sdk-oss</artifactId>
  <version>3.10.2</version>
</dependency>
  • 对象存储OSS → SDK文档 → 上传网络流
  • 对象存储OSS → iimooc-news-dev → 概览 → 访问端口: 外网访问 oss-cn-shanghai.aliyuncs.com

SDK的使用与项目整合

service-file  com/imooc/files/service/UploaderService.java
package com.imooc.files.service;

import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

public interface UploaderService {
    /**
     * 使用fastdfs 上传文件
     */
    public String uploadFdfs(MultipartFile file, String fileExtName) throws IOException;

    /**
     * 使用OSS 上传文件
     */
    public String uploadOSS(MultipartFile file,String userId, String fileExtName) throws IOException;
}
service-files  com/imooc/files/service/impl/UploaderServiceImpl.java
package com.imooc.files.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.imooc.files.resource.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.utils.extend.AliyunResource;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;

@Service
public class UploaderServiceImpl implements UploaderService {
    //注入客户端
    @Autowired
    public FastFileStorageClient fastFileStorageClient;
    @Autowired
    public FileResource fileResource;
    @Autowired
    public AliyunResource aliyunResource;
    @Autowired
    public Sid sid;

    @Override
    public String uploadFdfs(MultipartFile file, String fileExtName) throws IOException {
        StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileExtName, null);
        return storePath.getFullPath();
    }

    @Override
    public String uploadOSS(MultipartFile file, String userId, String fileExtName) throws IOException {
        // Endpoint以杭州为例,其它Region请按实际情况填写。
        // 外网访问:oss-cn-shanghai.aliyuncs.com
        String endpoint = fileResource.getEndpoint();
        // 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。
        String accessKeyId = aliyunResource.getAccessKeyID();
        String accessKeySecret = aliyunResource.getAccessKeySecret();

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint,
                accessKeyId,
                accessKeySecret);
        //  images/abc/10010/cat.png  路径不全 所以需要myObjectName拼接
        String fileName = sid.nextShort();
        String myObjectName = fileResource.getObjectName()
                + "/" + userId + "/" + fileName + "." + fileExtName;

        // 上传网络流。
        InputStream inputStream = file.getInputStream();
        ossClient.putObject(fileResource.getBucketName(),
                myObjectName,
                inputStream);

        // 关闭OSSClient。
        ossClient.shutdown();
        return myObjectName;
    }
}
service-files  com/imooc/files/resource/FileResource.java
package com.imooc.files.resource;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:file-${spring.profiles.active}.properties ") //这个是在application.yml里面的 自动匹配
@ConfigurationProperties(prefix = "file")
public class FileResource {
    private String host;
    private String endpoint;
    private String BucketName;
    private String objectName;
}Getter + Setter
file-dev.properties
# fastdfs storage ????(nginx???web??)
file.host=http://192.168.170.136:8888/

# aliyun OSS
file.endpoint=oss-cn-shanghai.aliyuncs.com

file.BucketName=iimooc-news-dev

# url name
file.objectName=images/abc

OSS整合实现文件上传

 // OSS执行上传
//  path = uploaderService.uploadOSS(file, userId, suffix);

service-files  com/imooc/files/controller/FileUploadController.java
package com.imooc.files.controller;


import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.files.resource.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Autowired
    private FileResource fileResource;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // fdfs执行上传
//                path = uploaderService.uploadFdfs(file, suffix);
                // OSS执行上传
                path = uploaderService.uploadOSS(file, userId, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);

        String finalPath = "";
        if (StringUtils.isNotBlank(path)){
            finalPath = fileResource.getHost() + path;
        }  else{
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);
        }
        return GraceJSONResult.ok(finalPath);
    }
}

=================================================================================
http://writer.imoocnews.com:9090/imooc-news/writer/accountInfo.html
此时更改头像上传后 头像会在OSS服务器的文件里面显示
file-dev.properties / file-prod.properties
# fastdfs storage ????(nginx???web??)
file.host=http://192.168.170.136:8888/

# aliyun OSS
file.endpoint=oss-cn-shanghai.aliyuncs.com

file.BucketName=iimooc-news-dev

# url name
file.objectName=images/abc

file.ossHost=https://iimooc-news-dev.oss-cn-shanghai.aliyuncs.com/
service-files  com/imooc/files/resource/FileResource.java
@Component
@PropertySource("classpath:file-${spring.profiles.active}.properties ") //这个是在application.yml里面的 自动匹配
@ConfigurationProperties(prefix = "file")
public class FileResource {
    private String host;
    private String endpoint;
    private String BucketName;
    private String objectName;
    private String OssHost;
}Getter + Setter
service-files  com/imooc/files/controller/FileUploadController.java
    //用OSS执行上传 而不是 fdfs执行上传
package com.imooc.files.controller;


import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.files.resource.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Autowired
    private FileResource fileResource;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // fdfs执行上传
//                path = uploaderService.uploadFdfs(file, suffix);
                // OSS执行上传
                path = uploaderService.uploadOSS(file, userId, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);

        String finalPath = "";
        if (StringUtils.isNotBlank(path)){
//            finalPath = fileResource.getHost() + path;
            finalPath = fileResource.getOssHost() + path;
        }  else{
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);
        }
        return GraceJSONResult.ok(finalPath);
    }
}

图片自动审核 【阿里内容安全】

多媒体内容风险智能识别服务,降低色情、暴力、恐怖 (由于太贵了就不买了 1000多呢)
dev-common  pom.xml
 <!-- 第三方云厂商相关的依赖 -->

        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.10.2</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-green</artifactId>
            <version>3.5.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.10</version>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
dev-common  com/imooc/utils/extend/AliImageReviewUtils.java
package com.imooc.utils.extend;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.green.model.v20180509.ImageSyncScanRequest;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.http.HttpResponse;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.http.ProtocolType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.imooc.enums.ArticleReviewLevel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Date;
import java.util.UUID;

@Component
public class AliImageReviewUtils {

//    文档地址:https://help.aliyun.com/document_detail/70292.html?spm=a2c4g.11186623.2.49.6f9c75fdjaW30p#reference-fzy-ztm-v2b

    @Autowired
    private AliyunResource aliyunResource;

    public boolean reviewImage(String imgUrl) throws Exception {
        IClientProfile profile = DefaultProfile
                .getProfile("cn-shanghai", "", "");
        DefaultProfile
                .addEndpoint("cn-shanghai", "cn-shanghai", "Green", "green.cn-shanghai.aliyuncs.com");
        IAcsClient client = new DefaultAcsClient(profile);

        ImageSyncScanRequest imageSyncScanRequest = new ImageSyncScanRequest();
        // 指定api返回格式
        imageSyncScanRequest.setAcceptFormat(FormatType.JSON);
        // 指定请求方法
        imageSyncScanRequest.setMethod(MethodType.POST);
        imageSyncScanRequest.setEncoding("utf-8");
        //支持http和https
        imageSyncScanRequest.setProtocol(ProtocolType.HTTP);


        JSONObject httpBody = new JSONObject();
        /**
         * 设置要检测的场景, 计费是按照该处传递的场景进行
         * 一次请求中可以同时检测多张图片,每张图片可以同时检测多个风险场景,计费按照场景计算
         * 例如:检测2张图片,场景传递porn、terrorism,计费会按照2张图片鉴黄,2张图片暴恐检测计算
         * porn: porn表示色情场景检测
         * logo: 商标
         * 其他详见官方文档
         */
        httpBody.put("scenes", Arrays.asList("terrorism"));

        /**
         * 设置待检测图片, 一张图片一个task
         * 多张图片同时检测时,处理的时间由最后一个处理完的图片决定
         * 通常情况下批量检测的平均rt比单张检测的要长, 一次批量提交的图片数越多,rt被拉长的概率越高
         * 这里以单张图片检测作为示例, 如果是批量图片检测,请自行构建多个task
         */
        JSONObject task = new JSONObject();
        task.put("dataId", UUID.randomUUID().toString());

        //设置图片链接
        task.put("url", imgUrl);
        task.put("time", new Date());
        httpBody.put("tasks", Arrays.asList(task));

        imageSyncScanRequest.setHttpContent(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(httpBody.toJSONString()),
                "UTF-8", FormatType.JSON);

        /**
         * 请设置超时时间, 服务端全链路处理超时时间为10秒,请做相应设置
         * 如果您设置的ReadTimeout小于服务端处理的时间,程序中会获得一个read timeout异常
         */
        imageSyncScanRequest.setConnectTimeout(3000);
        imageSyncScanRequest.setReadTimeout(10000);
        HttpResponse httpResponse = null;
        try {
            httpResponse = client.doAction(imageSyncScanRequest);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //服务端接收到请求,并完成处理返回的结果
        if (httpResponse != null && httpResponse.isSuccess()) {
            JSONObject scrResponse = JSON.parseObject(org.apache.commons.codec.binary.StringUtils.newStringUtf8(httpResponse.getHttpContent()));
            System.out.println(JSON.toJSONString(scrResponse, true));
            int requestCode = scrResponse.getIntValue("code");
            //每一张图片的检测结果
            JSONArray taskResults = scrResponse.getJSONArray("data");
            if (200 == requestCode) {
                for (Object taskResult : taskResults) {
                    //单张图片的处理结果
                    int taskCode = ((JSONObject) taskResult).getIntValue("code");
                    //图片要检测的场景的处理结果, 如果是多个场景,则会有每个场景的结果
                    JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
                    if (200 == taskCode) {
                        Object sceneResult = sceneResults.get(0);
//                        for (Object sceneResult : sceneResults) {
                        String scene = ((JSONObject) sceneResult).getString("scene");
                        String suggestion = ((JSONObject) sceneResult).getString("suggestion");
                        //根据scene和suggetion做相关处理
                        //do something
                        System.out.println("scene = [" + scene + "]");
                        System.out.println("suggestion = [" + suggestion + "]");

                        return suggestion.equalsIgnoreCase(ArticleReviewLevel.PASS.type) ? true : false;
//                        }
                    } else {
                        //单张图片处理失败, 原因视具体的情况详细分析
                        System.out.println("task process fail. task response:" + JSON.toJSONString(taskResult));
                        return false;
                    }
                }
            } else {
                /**
                 * 表明请求整体处理失败,原因视具体的情况详细分析
                 */
                System.out.println("the whole image scan request failed. response:" + JSON.toJSONString(scrResponse));
                return false;
            }
        }
        return false;
    }
}

解决github Push rejected报错 remote: error: GH013: Repository rule violations found for refs/heads/maste_github push declined due to repository rule violat-CSDN博客

dev-common  com/imooc/enums/ArticleReviewLevel.java
package com.imooc.enums;

/**
 * @Desc: 文章自动审核结果 枚举
 */
public enum ArticleReviewLevel {
    PASS("pass", "自动审核通过"),
    BLOCK("block", "自动审核不通过"),
    REVIEW("review", "建议人工复审");

    public final String type;
    public final String value;

    ArticleReviewLevel(String type, String value) {
        this.type = type;
        this.value = value;
    }
}
service-files  com/imooc/files/controller/FileUploadController.java
package com.imooc.files.controller;

import com.imooc.api.controller.files.FileUploadControllerApi;
import com.imooc.files.resource.FileResource;
import com.imooc.files.service.UploaderService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.extend.AliImageReviewUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class FileUploadController implements FileUploadControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FileUploadController.class);

    @Autowired
    private UploaderService uploaderService;

    @Autowired
    private FileResource fileResource;

    @Autowired
    private AliImageReviewUtils aliImageReviewUtils;

    @Override
    public GraceJSONResult uploadFace(String userId,
                                      MultipartFile file) throws Exception {
        String path = "";
        if (file != null){
        // 获得文件上传的名称
            String fileName = file.getOriginalFilename();
            //判断文件名不能为空
            if (StringUtils.isNotBlank(fileName)){
                String fileNameArr[] = fileName.split("\\.");
                //获得后缀名
                String suffix = fileNameArr[fileNameArr.length - 1];
                //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                if (!suffix.equalsIgnoreCase("png") &&
                        !suffix.equalsIgnoreCase("jpg") &&
                        !suffix.equalsIgnoreCase("jpeg")
                ){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_FORMATTER_FAILD);
                }
                // fdfs执行上传     要让外面得以访问 ①需要把内网的环境发布到公网 [内网穿透]  ②路由器端口映射到外网  ③fastdfs安装到公网里
//                path = uploaderService.uploadFdfs(file, suffix);
                // OSS执行上传
                path = uploaderService.uploadOSS(file, userId, suffix);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
            }
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_NULL_ERROR);
        }
        logger.info("path = " + path);

        String finalPath = "";
        if (StringUtils.isNotBlank(path)){
//            finalPath = fileResource.getHost() + path;
            finalPath = fileResource.getOssHost() + path;
        }  else{
            return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_UPLOAD_FAILD);
        }
           return GraceJSONResult.ok(finalPath);
       // return GraceJSONResult.ok(doAliImageReview(finalPath)); //这里加了图片审核咯
    }

    /**
     * fastdfs 默认存在于内网,无法被阿里云内容管理服务检查到
     * 需要配置到公网才行:
     * 1. 内网穿透,natppp/花生壳/ngrok
     * 2. 路由配置端口映射
     * 3. fdfs 发布到云服务器
     */

   /* 功能实现不了图片识别 因为没有开通内容安全需要企业认证
     public static final String FAILED_IMAGE_URL = "https://iimooc-news-dev.oss-cn-shanghai.aliyuncs.com/images/abc/240629F21AK1BHX4/Review_Failed.png"; //这里保存审核失败的照片 提前上传到Oss里直接用
    private String doAliImageReview(String pendingImageUrl){
        boolean result = false;
        try {
            result = aliImageReviewUtils.reviewImage(pendingImageUrl);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (!result){
            return FAILED_IMAGE_URL;
        }
        return pendingImageUrl;
    }
}
*/

创建阿里云子账号 【阿里内容安全】

RAM访问控制 → 用户
[实在不行了 凑合着搞一下吧 功能实现不了图片识别 因为没有开通内容安全需要企业认证]

登录名称:imooc-news-dev
显示名称:用于内容审核
√ OpenAPI访问调用
AccessKey ID:
AccessKeySeret:

点击左侧列表下方 授权 → 新增授权 【授权主体:用于内容审核 权限策略:搜:green … 管理内容安全的权限】

构建admin服务

  • 构建admin管理服务
  • 文章分类管理
  • 友情连接管理
  • 用户账号管理
  • 文章内容人工审核 [放在文章上传后的自动审核]
  • admin管理人员账号分配 [用户人脸]
service-admin  pom.xml
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-admin</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


</project>
service-admin  logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-admin"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-admin"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/service-admin.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
service-admin:8005
package com.imooc.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-admin  com/imooc/admin/controller/HelloController.java
package com.imooc.admin.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    public Object hello() {
        return GraceJSONResult.ok();
    }
}
----------------------------------------------------------------------------
http://admin.imoocnews.com:8005/hello

application-dev.yml
server:
  port: 8005

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379

表设计与账号预分配 【admin账号】

service-admin  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
service-admin  com/imooc/admin/controller/PWDTest.java
package com.imooc.admin.controller;

import org.springframework.security.crypto.bcrypt.BCrypt;

public class PWDTest {
    public static void main(String[] args) {
        String pwd = BCrypt.hashpw("admin", BCrypt.gensalt());//加盐
        System.out.println(pwd);
    }
}

持久层查询管理员 【admin账号】

http://admin.imoocnews.com:9090/imooc-news/admin/login.html
Windows下如何查看某个端口被谁占用 | 菜鸟教程 (runoob.com)

//更改一下mybatis-generator-database里面的generatorConfig-admin.xml
数据库表为:admin_user
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 通用mapper所在目录 -->
        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="com.imooc.my.mapper.MyMapper"/>
        </plugin>

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/imooc-news-dev"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!-- 对应生成的pojo所在包 -->
        <javaModelGenerator targetPackage="com.imooc.pojo" targetProject="mybatis-generator-database/src/main/java"/>

        <!-- 对应生成的mapper所在目录 -->
        <sqlMapGenerator targetPackage="mapper.admin" targetProject="mybatis-generator-database/src/main/resources"/>

        <!-- 配置mapper对应的java映射 -->
        <javaClientGenerator targetPackage="com.imooc.admin.mapper" targetProject="mybatis-generator-database/src/main/java" type="XMLMAPPER"/>

        <!-- 数据库表 -->
        <table tableName="admin_user"></table>

    </context>
</generatorConfiguration>
mybatis-generator-database的把AdminUser拷贝到dev-model的com/imooc/pojo下
mybatis-generator-database的把AdminUserMapper拷贝到service-admin的resources mapper/AdminUserMapper.xml
service-admin  com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);
}



service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
package com.imooc.admin.service.impl;

import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.service.AdminUserService;
import com.imooc.pojo.AdminUser;
import org.springframework.beans.factory.annotation.Autowired;
import tk.mybatis.mapper.entity.Example;

public class AdminUserServiceImpl implements AdminUserService {
    @Autowired
    public AdminUserMapper adminUserMapper;
    @Override
    public AdminUser queryAdminByUsername(String username) {
        Example adminExample = new Example(AdminUser.class);
        Example.Criteria Criteria = adminExample.createCriteria();
        Criteria.andEqualTo("username",username);
        AdminUser admin = adminUserMapper.selectOneByExample(adminExample);
        return admin;
    }
}
service-admin  com/imooc/admin/mapper/AdminUserMapper.java
package com.imooc.admin.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.AdminUser;
import org.springframework.stereotype.Repository;

@Repository
public interface AdminUserMapper extends MyMapper<AdminUser> {
}
service-admin  mapper/AdminUserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.admin.mapper.AdminUserMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.AdminUser" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="username" property="username" jdbcType="VARCHAR" />
    <result column="password" property="password" jdbcType="VARCHAR" />
    <result column="face_id" property="faceId" jdbcType="VARCHAR" />
    <result column="admin_name" property="adminName" jdbcType="VARCHAR" />
    <result column="created_time" property="createdTime" jdbcType="TIMESTAMP" />
    <result column="updated_time" property="updatedTime" jdbcType="TIMESTAMP" />
  </resultMap>
</mapper>

用户名密码登录 【admin账号】

Spring里遇到的一个问题,autowired时报找不到bean定义_autowired找不到bean-CSDN博客

其次上述问题一定要去找Controller Service ServiceImpl 和 启动类里面的有没有正确扫描包@MapperScan(basePackages = “com.imooc.admin.mapper”)
service-admin  com/imooc/admin/controller/AdminMngController.java
package com.imooc.admin.controller;

import com.imooc.admin.service.AdminUserService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.AdminMngControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@RestController
public class AdminMngController extends BaseController implements AdminMngControllerApi {
    final static Logger logger = LoggerFactory.getLogger(AdminMngController.class);

    @Autowired
    private AdminUserService adminUserService;

    @Autowired
    private RedisOperator redis;

    @Override
    public Object adminLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. TODO 验证BO中的用户名和密码不为空

        // 1.查询admin用户的信息
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        // 2.判断admin不为空,如果为空则登录失败
        if (admin == null) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
        // 3.判断密码是否匹配
        boolean isPwdMath = BCrypt.checkpw(adminLoginBO.getPassword(), admin.getPassword());
        if (isPwdMath){
            doLoginSettings(admin,request,response);
            return GraceJSONResult.ok();
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
    }

    /**
     * 用于admin用户登录过后的基本信息设置
     */
    private void doLoginSettings(AdminUser admin, HttpServletRequest request, HttpServletResponse response){
        // 保存token放入到redis中
        String token = UUID.randomUUID().toString();
        redis.set(REDIS_ADMIN_TOKEN + ":" + admin.getId(),token);

        // 保存admin登录基本token信息到cookie中
        setCookie(request, response, "atoken", token, COOKIE_MONTH);
        setCookie(request, response, "aid", admin.getId(), COOKIE_MONTH);
        setCookie(request, response, "aname", admin.getAdminName(), COOKIE_MONTH);
    }
}
---------------------------------------------------------------------------------
http://admin.imoocnews.com:9090/imooc-news/admin/login.html
service-admin  com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);
}
service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
package com.imooc.admin.service.impl;

import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.service.AdminUserService;
import com.imooc.pojo.AdminUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

@Service
public class AdminUserServiceImpl implements AdminUserService {
    @Autowired
    public AdminUserMapper adminUserMapper;
    @Override
    public AdminUser queryAdminByUsername(String username) {
        Example adminExample = new Example(AdminUser.class);
        Example.Criteria Criteria = adminExample.createCriteria();
        Criteria.andEqualTo("username",username);
        AdminUser admin = adminUserMapper.selectOneByExample(adminExample);
        return admin;
    }
}
service-admin  com/imooc/admin/mapper/AdminUserMapper.java
package com.imooc.admin.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.AdminUser;
import org.springframework.stereotype.Repository;

@Repository
public interface AdminUserMapper extends MyMapper<AdminUser> {
}


service-admin  resources/mapper/AdminUserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.admin.mapper.AdminUserMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.AdminUser" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="username" property="username" jdbcType="VARCHAR" />
    <result column="password" property="password" jdbcType="VARCHAR" />
    <result column="face_id" property="faceId" jdbcType="VARCHAR" />
    <result column="admin_name" property="adminName" jdbcType="VARCHAR" />
    <result column="created_time" property="createdTime" jdbcType="TIMESTAMP" />
    <result column="updated_time" property="updatedTime" jdbcType="TIMESTAMP" />
  </resultMap>
</mapper>
service-admin  com/imooc/admin/Application.java
package com.imooc.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.admin.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-admin  application-dev.yml
server:
  port: 8005

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
## setup CN from java, This is resource
website:
  domain-name: imoocnews.com

## open mybatis log in dev
#mybatis:
#  configuration:
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#

校验admin账号唯一 【admin账号】

http://admin.imoocnews.com:9090/imooc-news/admin/adminMng.html

service-admin  com/imooc/admin/controller/AdminMngController.java

  @Override
    public Object adminLogin(String username) {
        checkAdminExist(username);
        return GraceJSONResult.ok();
    }
    private void checkAdminExist(String username){
        AdminUser admin = adminUserService.queryAdminByUsername(username);
        if (admin != null){
            GraceException.display(ResponseStatusEnum.ADMIN_USERNAME_EXIST_ERROR);
        }

    }
service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.pojo.bo.AdminLoginBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "管理员admin维护",tags = {"管理员admin维护的Controller"})
@RequestMapping("adminMng")
public interface AdminMngControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/adminLogin")
    public Object adminLogin(@RequestBody AdminLoginBO adminLoginBO,
                             HttpServletRequest request,
                             HttpServletResponse response);

    @ApiOperation(value = "查询admin用户名是否存在",notes = "查询admin用户名是否存在",httpMethod = "POST")
    @PostMapping("/adminIsExist")
    public Object adminLogin(@RequestParam String username); //传回来
}
service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UseActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UseActiveInterceptor useActiveInterceptor(){
        return new UseActiveInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace");

        //        registry.addInterceptor(userTokenInterceptor())
//                .addPathPatterns("/user/getAccountInfo")
    }
}

创建admin账号 【admin账号】

用户管理 | 运营管理平台 (imoocnews.com)
http://admin.imoocnews.com:9090/imooc-news/admin/adminMng.html

service-admin  com/imooc/admin/controller/AdminMngController.java
package com.imooc.admin.controller;

import com.imooc.admin.service.AdminUserService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.AdminMngControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@RestController
public class AdminMngController extends BaseController implements AdminMngControllerApi {
    final static Logger logger = LoggerFactory.getLogger(AdminMngController.class);

    @Autowired
    private AdminUserService adminUserService;

    @Autowired
    private RedisOperator redis;

    @Override
    public GraceJSONResult adminLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. TODO 验证BO中的用户名和密码不为空

        // 1.查询admin用户的信息
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        // 2.判断admin不为空,如果为空则登录失败
        if (admin == null) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
        // 3.判断密码是否匹配
        boolean isPwdMath = BCrypt.checkpw(adminLoginBO.getPassword(), admin.getPassword());
        if (isPwdMath){
            doLoginSettings(admin,request,response);
            return GraceJSONResult.ok();
        }else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
    }


    /**
     * 用于admin用户登录过后的基本信息设置
     */
    private void doLoginSettings(AdminUser admin, HttpServletRequest request, HttpServletResponse response){
        // 保存token放入到redis中
        String token = UUID.randomUUID().toString();
        redis.set(REDIS_ADMIN_TOKEN + ":" + admin.getId(),token);

        // 保存admin登录基本token信息到cookie中
        setCookie(request, response, "atoken", token, COOKIE_MONTH);
        setCookie(request, response, "aid", admin.getId(), COOKIE_MONTH);
        setCookie(request, response, "aname", admin.getAdminName(), COOKIE_MONTH);
    }


    @Override
    public GraceJSONResult adminLogin(String username) {
        checkAdminExist(username);
        return GraceJSONResult.ok();
    }

    private void checkAdminExist(String username){
        AdminUser admin = adminUserService.queryAdminByUsername(username);
        if (admin != null){
            GraceException.display(ResponseStatusEnum.ADMIN_USERNAME_EXIST_ERROR);
        }

    }

    @Override
    public GraceJSONResult addNewAdmin(NewAdminBO newAdminBO,HttpServletRequest request,HttpServletResponse response) {
        // 0. TODO 验证BO中的用户名和密码不为空

        // 1. base64不为空,则代表人脸入库,否则需要用户输入密码和确认密码
        if (StringUtils.isBlank(newAdminBO.getImg64())){
            if (StringUtils.isBlank(newAdminBO.getPassword()) || StringUtils.isBlank(newAdminBO.getConfirmPassword())){
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_PASSWORD_NULL_ERROR);
            }
        }
        // 2. 密码不为空,则必须判断两次输入一致
        if (StringUtils.isNotBlank(newAdminBO.getPassword())) {
            if (!newAdminBO.getPassword().equalsIgnoreCase(newAdminBO.getConfirmPassword())) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_PASSWORD_ERROR);
            }
        }
        // 3. 校验用户名唯一
        checkAdminExist(newAdminBO.getUsername());

        // 4.调用service存入admin信息
        adminUserService.createAdminUser(newAdminBO);
        return GraceJSONResult.ok();
    }

}
service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
package com.imooc.api.controller.admin;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "管理员admin维护",tags = {"管理员admin维护的Controller"})
@RequestMapping("adminMng")
public interface AdminMngControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/adminLogin")
    public GraceJSONResult adminLogin(@RequestBody AdminLoginBO adminLoginBO,
                                      HttpServletRequest request,
                                      HttpServletResponse response);

    @ApiOperation(value = "查询admin用户名是否存在",notes = "查询admin用户名是否存在",httpMethod = "POST")
    @PostMapping("/adminIsExist")
    public GraceJSONResult adminLogin(@RequestParam String username); //传回来

    @ApiOperation(value = "创建admin",notes = "创建admin",httpMethod = "POST")
    @PostMapping("/addNewAdmin")
    public GraceJSONResult addNewAdmin(@RequestBody NewAdminBO newAdminBO,HttpServletRequest request,HttpServletResponse response); //传回来
}
service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
package com.imooc.admin.service.impl;

import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.service.AdminUserService;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;

@Service
public class AdminUserServiceImpl implements AdminUserService {
    @Autowired
    public AdminUserMapper adminUserMapper;
    @Autowired
    public Sid sid;
    @Override
    public AdminUser queryAdminByUsername(String username) {
        Example adminExample = new Example(AdminUser.class);
        Example.Criteria Criteria = adminExample.createCriteria();
        Criteria.andEqualTo("username",username);
        AdminUser admin = adminUserMapper.selectOneByExample(adminExample);
        return admin;
    }

    @Override
    public void createAdminUser(NewAdminBO newAdminBO) {
        String adminId = sid.nextShort(); //获得主键
        AdminUser adminUser = new AdminUser();
        adminUser.setId(adminId);
        adminUser.setUsername(newAdminBO.getUsername());
        adminUser.setAdminName(newAdminBO.getAdminName());
        // 如果密码不为空 则密码需要加密 存入数据库
        if (StringUtils.isNotBlank(newAdminBO.getPassword())){
            String pwd = BCrypt.hashpw(newAdminBO.getPassword(), BCrypt.gensalt());
            adminUser.setPassword(pwd);
        }

        // 如果人脸上传以后,则有faceId,需要和admin信息关联存储入库
        if (StringUtils.isNotBlank(newAdminBO.getFaceId())){
            adminUser.setFaceId(newAdminBO.getFaceId());
        }
        adminUser.setCreatedTime(new Date());
        adminUser.setUpdatedTime(new Date());

        int insert = adminUserMapper.insert(adminUser);
        if (insert != 1){
            GraceException.display(ResponseStatusEnum.ADMIN_CREATE_ERROR);
        }
    }
}
com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);

    /**
     * 新增管理员
     *
     * @param newAdminBO
     */
    public void createAdminUser(NewAdminBO newAdminBO);
}

查看admin列表 【admin账号】(分页查询)

service-admin  com/imooc/admin/controller/AdminMngController.java
 @Override
    public GraceJSONResult getAdminList(Integer page, Integer pageSize) {
        if (page == null){
            page = COMMON_START_PAGE;
        }
        if (pageSize == null){//由于是固定数值 可以去basecontroller加一下
            pageSize = COMMON_PAGE_SIZE;
        }
        adminUserService.queryAdminList(page, pageSize);
        return GraceJSONResult.ok();
    }
service-admin  com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);

    /**
     * 新增管理员
     *
     * @param newAdminBO
     */
    public void createAdminUser(NewAdminBO newAdminBO);


    /**
     * 分页查询admin列表
     * @param page
     * @param pageSize
     */
    public void queryAdminList(Integer page, Integer pageSize);
}
service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
@Override
    public void queryAdminList(Integer page, Integer pageSize) {
        Example adminExample = new Example(AdminUser.class);
        adminExample.orderBy("createdTime").asc();
        PageHelper.startPage(page, pageSize);
        List<AdminUser> adminUserList = adminUserMapper.selectByExample(adminExample);
        System.out.println(adminUserList);
    } //下面一节会有改动
service-api  com/imooc/api/config/Swagger2.java
package com.imooc.api.config;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration //Springboot啓動的時候會被掃描到并且加載
@EnableSwagger2
public class Swagger2 {

    //    http://localhost:8088/swagger-ui.html     原路径
    //    http://localhost:8088/doc.html            新路径

    // 配置swagger2核心配置 docket
    @Bean
    public Docket createRestApi() {
        Predicate<RequestHandler> adminPredicate = RequestHandlerSelectors.basePackage("com.imooc.admin.controller");
//        Predicate<RequestHandler> articlePredicate = RequestHandlerSelectors.basePackage("com.imooc.article.controller");
        Predicate<RequestHandler> userPredicate = RequestHandlerSelectors.basePackage("com.imooc.user.controller");
        Predicate<RequestHandler> filesPredicate = RequestHandlerSelectors.basePackage("com.imooc.files.controller");

        return new Docket(DocumentationType.SWAGGER_2)  // 指定api类型为swagger2
                .apiInfo(apiInfo())                 // 用于定义api文档汇总信息
                .select()
                .apis(Predicates.or(userPredicate, adminPredicate, filesPredicate))
//                .apis(Predicates.or(adminPredicate, articlePredicate, userPredicate, filesPredicate))
                .paths(PathSelectors.any())         // 所有controller
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕课新闻·自媒体接口api")                       // 文档页标题
                .contact(new Contact("imooc",
                        "https://www.imooc.com",
                        "abc@imooc.com"))                   // 联系人信息
                .description("专为慕课新闻·自媒体平台提供的api文档")      // 详细信息
                .version("1.0.1")                               // 文档版本号
                .termsOfServiceUrl("https://www.imooc.com")     // 网站地址
                .build();
    }
}
service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
package com.imooc.api.controller.admin;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "管理员admin维护",tags = {"管理员admin维护的Controller"})
@RequestMapping("adminMng")
public interface AdminMngControllerApi {
    @ApiOperation(value = "hello方法的接口",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/adminLogin")
    public GraceJSONResult adminLogin(@RequestBody AdminLoginBO adminLoginBO,
                                      HttpServletRequest request,
                                      HttpServletResponse response);

    @ApiOperation(value = "查询admin用户名是否存在",notes = "查询admin用户名是否存在",httpMethod = "POST")
    @PostMapping("/adminIsExist")
    public GraceJSONResult adminLogin(@RequestParam String username); //传回来

    @ApiOperation(value = "创建admin",notes = "创建admin",httpMethod = "POST")
    @PostMapping("/addNewAdmin")
    public GraceJSONResult addNewAdmin(@RequestBody NewAdminBO newAdminBO,HttpServletRequest request,HttpServletResponse response);

    @ApiOperation(value = "查询admin列表",notes = "查询admin列表",httpMethod = "POST")
    @PostMapping("/getAdminList")
    public GraceJSONResult getAdminList(@ApiParam(name = "page", value = "查询下一页的第几页",  required = false) @RequestParam Integer page,
                                        @ApiParam(name = "pageSize", value = "分页查询每一页显示的条数", required = false) @RequestParam Integer pageSize);
}

-----------------------------------------------------------------
http://admin.imoocnews.com:8005/doc.html

封装PagedGridResult分页数据_调试分页接口 【admin账号】

service-admin  com/imooc/admin/service/impl/AdminUserServiceImpl.java
    @Override
    public PagedGridResult queryAdminList(Integer page, Integer pageSize) {
        Example adminExample = new Example(AdminUser.class);
        adminExample.orderBy("createdTime").asc();
        PageHelper.startPage(page, pageSize);
        List<AdminUser> adminUserList = adminUserMapper.selectByExample(adminExample);
        return setterPagedGrid(adminUserList, page);
    }

    private PagedGridResult setterPagedGrid( List<?> adminUserList, Integer page){ //类型是? 后期不确定是什么泛型
        PageInfo<?> pageList = new PageInfo<>(adminUserList);
        PagedGridResult gridResult = new PagedGridResult();
        gridResult.setRows(adminUserList);
        gridResult.setPage(page);
        gridResult.setRecords(pageList.getPages());
        gridResult.setTotal(pageList.getTotal());
        return gridResult;

    }
service-admin  com/imooc/admin/service/AdminUserService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.utils.PagedGridResult;

public interface AdminUserService {
    /**
     * 获得管理员的用户信息
     * @param username
     * @return
     */
    public AdminUser queryAdminByUsername(String username);

    /**
     * 新增管理员
     *
     * @param newAdminBO
     */
    public void createAdminUser(NewAdminBO newAdminBO);


    /**
     * 分页查询admin列表
     * @param page
     * @param pageSize
     */
    public PagedGridResult queryAdminList(Integer page, Integer pageSize);
}
dev-common  com/imooc/utils/PagedGridResult.java
package com.imooc.utils;

import java.util.List;

/**
 * 
 * @Title: PagedGridResult.java
 * @Package com.imooc.utils
 * @Description: 用来返回分页Grid的数据格式
 * Copyright: Copyright (c) 2019
 */
public class PagedGridResult {
    
    private int page;            // 当前页数
    private long total;            // 总页数
    private long records;        // 总记录数
    private List<?> rows;        // 每行显示的内容
}Getter + Setter
service-api  com/imooc/api/config/InterceptorConfig.java
//拦截器新增地址
package com.imooc.api.config;

import com.imooc.api.interceptors.AdminTokenInterceptor;
import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UserActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UserActiveInterceptor userActiveInterceptor() {
        return new UserActiveInterceptor();
    }

    @Bean
    public AdminTokenInterceptor adminTokenInterceptor() {
        return new AdminTokenInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace");
        registry.addInterceptor(adminTokenInterceptor())//继续添加拦截器:查询admin列表 创建新admin用户
                .addPathPatterns("/adminMng/adminIsExist")
                .addPathPatterns("/adminMng/addNewAdmin")
                .addPathPatterns("/adminMng/getAdminList");

    }
}
service-api  com/imooc/api/interceptors/AdminTokenInterceptor.java
package com.imooc.api.interceptors;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import static com.imooc.api.BaseController.REDIS_ADMIN_TOKEN;

public class AdminTokenInterceptor extends BaseInterceptor implements HandlerInterceptor {

    /**
     * 拦截请求,在访问controller调用之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String adminUserId = request.getHeader("adminUserId");
        String adminUserToken = request.getHeader("adminUserToken");

        System.out.println("=====================================================================");
        System.out.println("AdminTokenInterceptor - adminUserId = " + adminUserId);
        System.out.println("AdminTokenInterceptor - adminUserToken = " + adminUserToken);
        System.out.println("=====================================================================");

        boolean run = verifyUserIdToken(adminUserId, adminUserToken, REDIS_ADMIN_TOKEN);
        return run;
    }

    /**
     * 请求访问controller之后,渲染视图之前
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     * 请求访问controller之后,渲染视图之后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

账号注销 【admin账号】(删掉redis和cookie数据)

service-admin  com/imooc/admin/controller/AdminMngController.java
package com.imooc.admin.controller;

import com.imooc.admin.service.AdminUserService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.AdminMngControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.enums.FaceVerifyType;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.utils.FaceVerifyUtils;
import com.imooc.utils.PagedGridResult;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@RestController
public class AdminMngController extends BaseController implements AdminMngControllerApi {

    final static Logger logger = LoggerFactory.getLogger(AdminMngController.class);

    @Autowired
    private RedisOperator redis;

    @Autowired
    private AdminUserService adminUserService;

    @Autowired
    private FaceVerifyUtils faceVerifyUtils;

    @Override
    public GraceJSONResult adminLogin(AdminLoginBO adminLoginBO,
                                      HttpServletRequest request,
                                      HttpServletResponse response) {
        // 0. TODO 验证BO中的用户名和密码不为空

        // 1. 查询admin用户的信息
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        // 2. 判断admin不为空,如果为空则登录失败
        if (admin == null) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }

        // 3. 判断密码是否匹配
        boolean isPwdMatch = BCrypt.checkpw(adminLoginBO.getPassword(), admin.getPassword());
        if (isPwdMatch) {
            doLoginSettings(admin, request, response);
            return GraceJSONResult.ok();
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_NOT_EXIT_ERROR);
        }
    }


    /**
     * 用于admin用户登录过后的基本信息设置
     * @param admin
     * @param request
     * @param response
     */
    private void doLoginSettings(AdminUser admin,
                                 HttpServletRequest request,
                                 HttpServletResponse response) {
        // 保存token放入到redis中
        String token = UUID.randomUUID().toString();
        redis.set(REDIS_ADMIN_TOKEN + ":" + admin.getId(), token);

        // 保存admin登录基本token信息到cookie中
        setCookie(request, response, "atoken", token, COOKIE_MONTH);
        setCookie(request, response, "aid", admin.getId(), COOKIE_MONTH);
        setCookie(request, response, "aname", admin.getAdminName(), COOKIE_MONTH);
    }

    @Override
    public GraceJSONResult adminIsExist(String username) {
        checkAdminExist(username);
        return GraceJSONResult.ok();
    }

    private void checkAdminExist(String username) {
        AdminUser admin = adminUserService.queryAdminByUsername(username);

        if (admin != null) {
            GraceException.display(ResponseStatusEnum.ADMIN_USERNAME_EXIST_ERROR);
        }
    }

    @Override
    public GraceJSONResult addNewAdmin(NewAdminBO newAdminBO,
                                       HttpServletRequest request,
                                       HttpServletResponse response) {

        // 0. TODO 验证BO中的用户名和密码不为空

        // 1. base64不为空,则代表人脸入库,否则需要用户输入密码和确认密码
        if (StringUtils.isBlank(newAdminBO.getImg64())) {
            if (StringUtils.isBlank(newAdminBO.getPassword()) ||
                    StringUtils.isBlank(newAdminBO.getConfirmPassword())
            ) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_PASSWORD_NULL_ERROR);
            }
        }

        // 2. 密码不为空,则必须判断两次输入一致
        if (StringUtils.isNotBlank(newAdminBO.getPassword())) {
            if (!newAdminBO.getPassword()
                    .equalsIgnoreCase(newAdminBO.getConfirmPassword())) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_PASSWORD_ERROR);
            }
        }

        // 3. 校验用户名唯一
        checkAdminExist(newAdminBO.getUsername());

        // 4. 调用service存入admin信息
        adminUserService.createAdminUser(newAdminBO);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult getAdminList(Integer page, Integer pageSize) {

        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult result = adminUserService.queryAdminList(page, pageSize);
        return GraceJSONResult.ok(result);
    }

    @Override
    public GraceJSONResult adminLogout(String adminId,
                                       HttpServletRequest request,
                                       HttpServletResponse response) {

        // 从redis中删除admin的会话token
        redis.del(REDIS_ADMIN_TOKEN + ":" + adminId);

        // 从cookie中清理adming登录的相关信息
        deleteCookie(request, response, "atoken");
        deleteCookie(request, response, "aid");
        deleteCookie(request, response, "aname");

        return GraceJSONResult.ok();
    }
}
service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "管理员admin维护", tags = {"管理员admin维护的controller"})
@RequestMapping("adminMng")
public interface AdminMngControllerApi {

    @ApiOperation(value = "hello方法的接口", notes = "hello方法的接口", httpMethod = "POST")
    @PostMapping("/adminLogin")
    public GraceJSONResult adminLogin(@RequestBody AdminLoginBO adminLoginBO,
                                      HttpServletRequest request,
                                      HttpServletResponse response);

    @ApiOperation(value = "查询admin用户名是否存在", notes = "查询admin用户名是否存在", httpMethod = "POST")
    @PostMapping("/adminIsExist")
    public GraceJSONResult adminIsExist(@RequestParam String username);

    @ApiOperation(value = "创建admin", notes = "创建admin", httpMethod = "POST")
    @PostMapping("/addNewAdmin")
    public GraceJSONResult addNewAdmin(@RequestBody NewAdminBO newAdminBO,
                                       HttpServletRequest request,
                                       HttpServletResponse response);

    @ApiOperation(value = "查询admin列表", notes = "查询admin列表", httpMethod = "POST")
    @PostMapping("/getAdminList")
    public GraceJSONResult getAdminList(
            @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
            @RequestParam Integer page,
            @ApiParam(name = "pageSize", value = "分页查询每一页显示的条数", required = false)
            @RequestParam Integer pageSize);

    @ApiOperation(value = "admin退出登录", notes = "admin退出登录", httpMethod = "POST")
    @PostMapping("/adminLogout")
    public GraceJSONResult adminLogout(@RequestParam String adminId,
                                       HttpServletRequest request,
                                       HttpServletResponse response);
                                        HttpServletResponse response);
}
service-api  com/imooc/api/BaseController.java
public void setCookieValue(HttpServletRequest request,
                               HttpServletResponse response,
                               String cookieName,
                               String cookieValue,
                               Integer maxAge) {
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setMaxAge(maxAge);
//        cookie.setDomain("imoocnews.com");
        cookie.setDomain(DOMAIN_NAME);
        cookie.setPath("/");//都用cookie
        response.addCookie(cookie);//把cookie传入
    }

    public void deleteCookie(HttpServletRequest request,HttpServletResponse response,String cookieName){
        try {
            String deleteValue = URLEncoder.encode("", "utf-8");
            setCookieValue(request, response, cookieName, deleteValue, COOKIE_DELETE);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

人脸业务流程图梳理

Chrome开启视频调试模式

在谷歌浏览器中打开【每一次重启电脑都要操作】
chrome://flags/#unsafely-treat-insecure-origin-as-secure
|————————————————————————————————|
| http://admin.imoocnews.com:9090,http://admin.imoocnews.com |
|—— ——————————————————————————————|
http://admin.imoocnews.com:9090/imooc-news/admin/adminMng.html
可以获取人脸

MongoDB概念 [人脸数据存储]可以存储JSON数据

  • NoSql 数据库
  • 内存级别查询
  • 不支持事务
  • 非并发读写 请求并发数据量大
  • GridFS 小文件存储

MongoDB术语

数据库 ElasticSearch MongoDB
database es库 database
table表 index索引 collection数据集合
row行 (记录) document文档 (json) document文档 (json)
column 字段列 field域 field域
index索引 - index索引
join表关联查询 - -
pk主键 _id _id
MongoDB数据结构
UserList:[
    {
        userId: "1001",
        username: "lee",
        age: 18
    },
    {
        userId: "1002",
        username: "jay",
        age: 20,
        sex: "boy"
    }
]

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

UserList -->   collection
{}       -->   document
属性        -->   column

MogoDB安装与配置使用

https://www.mongodb.com/try/download/enterprise

将mongodb-linux-x86_64-rhel70-4.2.8传入虚拟机
[imooc@imooc ~]$ tar -zxvf mongodb-linux-x86_64-rhel70-4.2.8.tgz 
[imooc@imooc ~]$ sudo mv mongodb-linux-x86_64-rhel70-4.2.8 /usr/local/mongodb
[imooc@imooc ~]$ cd /usr/local/
[imooc@imooc local]$ ll
drwxrwxr-x. 3 imooc imooc      135 7月  16 19:46 mongodb
[imooc@imooc local]$ cd mongodb/
[imooc@imooc mongodb]$ ll
总用量 312
drwxrwxr-x. 2 imooc imooc    231 7月  16 19:46 bin
-rw-r--r--. 1 imooc imooc  30608 6月  12 2020 LICENSE-Community.txt
-rw-r--r--. 1 imooc imooc  16726 6月  12 2020 MPL-2
-rw-r--r--. 1 imooc imooc   2617 6月  12 2020 README
-rw-r--r--. 1 imooc imooc  75405 6月  12 2020 THIRD-PARTY-NOTICES
-rw-r--r--. 1 imooc imooc 183512 6月  12 2020 THIRD-PARTY-NOTICES.gotools
[imooc@imooc mongodb]$ cd bin/
[imooc@imooc bin]$ pwd
/usr/local/mongodb/bin

[imooc@imooc bin]$ sudo vim /etc/profile
最下面添加:
export JAVA_HOME=/usr/java/jdk1.8.0_222-ea
export CLASSPATH=.:%JAVA_HOME%/lib/dt.jar:%JAVA_HOME%/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin
#set mogodb config
export PATH=/usr/local/mongodb/bin:$PATH

#修改 /etc/profile 文件后,需要重新加载这个文件才能使新配置生效。你可以执行以下命令:
[imooc@imooc bin]$ source /etc/profile

[imooc@imooc bin]$ mongo --version
MongoDB shell version v4.2.8
git version: 43d25964249164d76d5e04dd6cf38f6111e21f5f
OpenSSL version: OpenSSL 1.0.1e-fips 11 Feb 2013
allocator: tcmalloc
modules: none
build environment:
    distmod: rhel70
    distarch: x86_64
    target_arch: x86_64

[imooc@imooc bin]$ cd /usr/local/mongodb/
[imooc@imooc mongodb]$ pwd
/usr/local/mongodb

#创建数据存储目录
[imooc@imooc mongodb]$ mkdir data/db -p #出来一个data
[imooc@imooc mongodb]$ ll
总用量 312
drwxrwxr-x. 2 imooc imooc    231 7月  16 19:46 bin
drwxrwxr-x. 3 imooc imooc     16 7月  16 20:00 data
[imooc@imooc mongodb]$ cd data
[imooc@imooc data]$ ll
总用量 0
drwxrwxr-x. 2 imooc imooc 6 7月  16 20:00 db
[imooc@imooc data]$ mkdir logs
[imooc@imooc data]$ ll
总用量 0
drwxrwxr-x. 2 imooc imooc 6 7月  16 20:00 db
drwxrwxr-x. 2 imooc imooc 6 7月  16 20:00 logs
[imooc@imooc data]$ cd logs/
[imooc@imooc logs]$ pwd
/usr/local/mongodb/data/logs
[imooc@imooc logs]$ touch mongodb.log
[imooc@imooc logs]$ ll
总用量 0
-rw-rw-r--. 1 imooc imooc 0 7月  16 20:01 mongodb.log
[imooc@imooc logs]$ cd ..
[imooc@imooc logs]$ cd ..

[imooc@imooc mongodb]$ vim mongodb.conf
port=27017
# datasource path
dbpath=/user/local/mongodb/data/db
# log path
logpath=/usr/local/mongodb/data/logs/mongodb.log
# append log
logappend=true
# cut useless log
quiet=true
# back desktop auto run
fork=true
# Maxcontect
maxConns=100
# Not open Verify permissions
noauth=true
# open Verify permissions
# auth=true
# open log => true
journal=true
# clash
bind_ip=0.0.0.0

[imooc@imooc mongodb]$ sudo yum install net-snmp
 
#错误:软件包:1:net-snmp-agent-libs-5.7.2-49.el7_9.4.x86_64 (updates)
          需要:libmysqlclient.so.18(libmysqlclient_18)(64bit)
#错误:软件包:1:net-snmp-5.7.2-49.el7_9.4.x86_64 (updates)
          需要:libmysqlclient.so.18()(64bit)
#错误:软件包:1:net-snmp-agent-libs-5.7.2-49.el7_9.4.x86_64 (updates)
          需要:libmysqlclient.so.18()(64bit)
# cd /usr/local/mongodb/
[imooc@imooc mongodb]$ mongod -f mongodb.conf
about to fork child process, waiting until server is ready for connections.
forked process: 4989
child process started successfully, parent exiting

[imooc@imooc mongodb]$ ps aux | grep mongod
imooc      4989  1.8  4.2 1550916 78280 ?       Sl   20:39   0:00 mongod -f mongodb.conf
imooc      5105  0.0  0.0 112824   988 pts/0    S+   20:40   0:00 grep --color=auto mongod

[imooc@imooc mongodb]$ ps -ef|grep mongodb
imooc      4989      1  0 20:39 ?        00:00:02 mongod -f mongodb.conf
imooc      5201   2948  0 20:44 pts/0    00:00:00 grep --color=auto mongodb


尝试连接到 MongoDB 实例: 
[imooc@imooc mongodb]$ mongo --port 27017

可视化管理工具【MongoDB】

在Navicat里新建链接MongoDB
主机:192.168.170.135
右键新建数据库school → 集合 → 右键新建 左上角保存student
[imooc@imooc mongodb]$ vim mongodb.conf
##### 启用用户账号权限
# Not open Verify permissions
# noauth=true
# open Verify permissions
  auth=true
#重启服务
[imooc@imooc mongodb]$ ps -ef|grep mongodb
imooc      4989      1  0 20:39 ?        00:00:05 mongod -f mongodb.conf
imooc      5380   2948  0 20:54 pts/0    00:00:00 grep --color=auto mongodb
[imooc@imooc mongodb]$ kill -2 4989
[imooc@imooc mongodb]$ ps -ef|grep mongodb
imooc      5395   2948  0 20:54 pts/0    00:00:00 grep --color=auto mongodb
[imooc@imooc mongodb]$ mongod -f mongodb.conf
about to fork child process, waiting until server is ready for connections.
forked process: 5419
child process started successfully, parent exiting

[imooc@imooc mongodb]$ mongo
MongoDB shell version v4.2.8
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("c87ffbd9-69cd-4e29-badd-5b86a314f428") }
MongoDB server version: 4.2.8
> use admin
switched to db admin
> db.createUser({user:"root",pwd:"root",roles:["root"]})
Successfully added user: { "user" : "root", "roles" : [ "root" ] }
> db.auth("root","root")
1
> show users
{
    "_id" : "admin.root",
    "userId" : UUID("2ced1f0a-8de4-4fab-9cb8-8e420fe9dcba"),
    "user" : "root",
    "db" : "admin",
    "roles" : [
        {
            "role" : "root",
            "db" : "admin"
        }
    ],
    "mechanisms" : [
        "SCRAM-SHA-1",
        "SCRAM-SHA-256"
    ]
}
> 
#后面关闭连接 编辑数据库 新增密码登录 root root

整合SpringBoot 【GridFS】

<!-- 引入 mongodb 依赖 -->
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver</artifactId>
        </dependency>
        <dependency>
service-api  com/imooc/api/controller/files/FileUploadControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploadControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    //不可以通过swagger2调用的
    /**
     * 文件上传到mongodb的gridfs中
     * @param newAdminBO
     * @return
     * @throws Exception
     */
    @PostMapping("/uploadToGridFS")
    public GraceJSONResult uploadToGridFS(@RequestBody NewAdminBO newAdminBO) throws Exception;
}
service-files  application.yml
  data:
    mongodb:
      uri: mongodb://root:root@192.168.170.135:27017
      database: imooc-news

实现人脸入库 【GridFS】

service-files  com/imooc/files/controller/FileUploadController.java

...
 @Autowired
    private GridFSBucket gridFSBucket;
...

 @Override
    public GraceJSONResult uploadToGridFS(NewAdminBO newAdminBO) throws Exception {
        // 获得图片的base64字符串
        String file64 = newAdminBO.getImg64();
        // 将base64字符串转换为byte数组
        byte[] bytes = new BASE64Decoder().decodeBuffer(file64.trim());
        // 转换为输入流
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        //上传到gridfs中
        ObjectId fileId = gridFSBucket.uploadFromStream(newAdminBO.getUsername() + ".png", inputStream);
        // 获取文件在gridfs中的主键id
        String fileIdStr = fileId.toString();
        // 下次提交的时候会提交到后端
        return GraceJSONResult.ok(fileIdStr);
    }

http://admin.imoocnews.com:9090/imooc-news/admin/adminMng.html
注册并且提交人脸信息
去Navicat → MongoDB → imooc-news → GridFS存储桶 → fs → admin456.png
service-files  com/imooc/files/controller/FileUploadController.java
...
    @Override
    public GraceJSONResult uploadToGridFS(NewAdminBO newAdminBO) throws Exception {
        // 获得图片的base64字符串
        String file64 = newAdminBO.getImg64();
        // 将base64字符串转换为byte数组
        byte[] bytes = new BASE64Decoder().decodeBuffer(file64.trim());
        // 转换为输入流
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        //上传到gridfs中
        ObjectId fileId = gridFSBucket.uploadFromStream(newAdminBO.getUsername() + ".png", inputStream);
        // 获取文件在gridfs中的主键id
        String fileIdStr = fileId.toString();
        // 下次提交的时候会提交到后端
        return GraceJSONResult.ok(fileIdStr);
    }
service-api  com/imooc/api/controller/files/FileUploadControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploadControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    //不可以通过swagger2调用的

    /**
     * 文件上传到mongodb的gridfs中
     * @param newAdminBO
     * @return
     * @throws Exception
     */
    @PostMapping("/uploadToGridFS")
    public GraceJSONResult uploadToGridFS(@RequestBody NewAdminBO newAdminBO) throws Exception;
}
service-files  com/imooc/files/GridFSConfig.java
package com.imooc.files;

import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component //可以被容器访问到
public class GridFSConfig {
    @Value("${spring.data.mongodb.database}")
    private String mongodb;

    @Bean
    public GridFSBucket gridFSBucket(MongoClient mongoClient){
        MongoDatabase mongoDatabase = mongoClient.getDatabase(mongodb);
        GridFSBucket bucket = GridFSBuckets.create(mongoDatabase);//存入mongodatabase
        return bucket;
    }
}

查看admin人脸信息 【GridFS】

service-files  com/imooc/files/controller/FileUploaderController.java
 @Override
    public GraceJSONResult uploadToGridFS(NewAdminBO newAdminBO)
            throws Exception {

        // 获得图片的base64字符串
        String file64 = newAdminBO.getImg64();

        // 将base64字符串转换为byte数组
        byte[] bytes = new BASE64Decoder().decodeBuffer(file64.trim());

        // 转换为输入流
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);

        // 上传到gridfs中
        ObjectId fileId = gridFSBucket.uploadFromStream(newAdminBO.getUsername() + ".png", inputStream);

        // 获得文件在gridfs中的主键id
        String fileIdStr = fileId.toString();

        return GraceJSONResult.ok(fileIdStr);
    }

    @Override
    public void readInGridFS(String faceId,
                             HttpServletRequest request,
                             HttpServletResponse response) throws Exception {

        // 0. 判断参数
        if (StringUtils.isBlank(faceId) || faceId.equalsIgnoreCase("null")) {
            GraceException.display(ResponseStatusEnum.FILE_NOT_EXIST_ERROR);
        }

        // 1. 从gridfs中读取
        File adminFace = readGridFSByFaceId(faceId);

        // 2. 把人脸图片输出到浏览器
        FileUtils.downloadFileByStream(response, adminFace);
    }

    private File readGridFSByFaceId(String faceId) throws Exception {

        GridFSFindIterable gridFSFiles
                = gridFSBucket.find(Filters.eq("_id", new ObjectId(faceId)));

        GridFSFile gridFS = gridFSFiles.first();

        if (gridFS == null) {
            GraceException.display(ResponseStatusEnum.FILE_NOT_EXIST_ERROR);
        }

        String fileName = gridFS.getFilename();
        System.out.println(fileName);

        // 获取文件流,保存文件到本地或者服务器的临时目录
        File fileTemp = new File("/workspace/temp_face");
        if (!fileTemp.exists()) {
            fileTemp.mkdirs();
        }

        File myFile = new File("/workspace/temp_face/" + fileName);

        // 创建文件输出流
        OutputStream os = new FileOutputStream(myFile);
        // 下载到服务器或者本地
        gridFSBucket.downloadToStream(new ObjectId(faceId), os);

        return myFile;
    }
service-api  com/imooc/api/controller/files/FileUploaderControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploaderControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    //不可以通过swagger2调用的

    /**
     * 文件上传到mongodb的gridfs中
     * @param newAdminBO
     * @return
     * @throws Exception
     */
    @PostMapping("/uploadToGridFS")
    public GraceJSONResult uploadToGridFS(@RequestBody NewAdminBO newAdminBO) throws Exception;

    @GetMapping("/readInGridFS")
    public void readInGridFS(String faceId, HttpServletRequest request, HttpServletResponse response) throws Exception;
}

//AdminCookieToken也可以获得faceId

阿里AI人脸识别介绍

人脸人体-阿里云视觉智能开放平台 (aliyun.com)

视觉智能开放平台-控制台 (aliyun.com)

能力展示-阿里云视觉智能开放平台 (aliyun.com)

获得人脸faceId【人脸登录】

service-api  com/imooc/api/controller/admin/AdminMngControllerApi.java
@ApiOperation(value = "admin管理员的人脸登录", notes = "admin管理员的人脸登录", httpMethod = "POST")
    @PostMapping("/adminFaceLogin")
    public GraceJSONResult adminFaceLogin(@RequestBody AdminLoginBO adminLoginBO,
                                          HttpServletRequest request,
                                          HttpServletResponse response);
service-admin  com/imooc/admin/controller/AdminMngController.java
 @Override
    public GraceJSONResult adminFaceLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. 判断用户名和人脸信息不能为空
        if(StringUtils.isBlank(adminLoginBO.getUsername())){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_USERNAME_NULL_ERROR);
        }
        String tempFace64 = adminLoginBO.getImg64();
        if (StringUtils.isBlank(tempFace64)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 1. 从数据库中查询出faceId
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        String adminFaceId = admin.getFaceId();
        if (StringUtils.isBlank(adminFaceId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 2. 请求文件服务,获得人懒数据的base64数据

        // 3. 调用阿里ai进行人脸对比识别,判断可信度,从而实现人脸登录

        // 4. admin登录后的数据设置,redis与cookie

        return null;
    }
service-api  com/imooc/api/controller/files/FileUploaderControllerApi.java
package com.imooc.api.controller.files;


import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewAdminBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploaderControllerApi {
    @ApiOperation(value = "上传用户头像",notes = "hello方法的接口",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    //不可以通过swagger2调用的

    /**
     * 文件上传到mongodb的gridfs中
     * @param newAdminBO
     * @return
     * @throws Exception
     */
    @PostMapping("/uploadToGridFS")
    public GraceJSONResult uploadToGridFS(@RequestBody NewAdminBO newAdminBO) throws Exception;

    @GetMapping("/readInGridFS")
    public void readInGridFS(String faceId, HttpServletRequest request, HttpServletResponse response) throws Exception;

    /**
     * 从gridfs中读取图片内容 返回base64数据
     * @param faceId
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @GetMapping("/readFace64InGridFS")
    public GraceJSONResult readFace64InGridFS(String faceId, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
service-files  com/imooc/files/controller/FileUploaderController.java
@Override
    public GraceJSONResult readFace64InGridFS(String faceId, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 0. 获得gridfs中人脸文件
        File myface = readGridFSByFaceId(faceId);

        // 1. 转换人脸为base64
        String base64Face = FileUtils.fileToBase64(myface);
        return GraceJSONResult.ok(base64Face);
    }

整合RestTemplate服务通信 【人脸登录】

service-admin  com/imooc/admin/controller/AdminMngController.java
@RestController
public class AdminMngController extends BaseController implements AdminMngControllerApi {

@Autowired
    private RestTemplate restTemplate;

@Override
    public GraceJSONResult adminFaceLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. 判断用户名和人脸信息不能为空
        if(StringUtils.isBlank(adminLoginBO.getUsername())){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_USERNAME_NULL_ERROR);
        }
        String tempFace64 = adminLoginBO.getImg64();
        if (StringUtils.isBlank(tempFace64)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 1. 从数据库中查询出faceId
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        String adminFaceId = admin.getFaceId();
        if (StringUtils.isBlank(adminFaceId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 2. 请求文件服务,获得人懒数据的base64数据
        String fileServerUrlExecute = "http://files.imoocnews.com:8004/fs/readFace64InGridFS?faceId=" + adminFaceId;
        ResponseEntity<GraceJSONResult> responseEntity = restTemplate.getForEntity(fileServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        String base64DB = (String)bodyResult.getData();
        // 3. 调用阿里ai进行人脸对比识别,判断可信度,从而实现人脸登录

        // 4. admin登录后的数据设置,redis与cookie

        return null;
    }
}
service-api  com/imooc/api/config/CloudConfig.java
package com.imooc.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CloudConfig {

    public CloudConfig() {
    }

    /**
     * 会基于OKHttp3的配置来实例RestTemplate
     * @return
     */
    @Bean
    public RestTemplate restTemplate() {

        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}

实现人脸对比进行登录【人脸对比】没买人脸识别服务,简单写了一下

service-admin  com/imooc/admin/controller/AdminMngController.java
@Override
    public GraceJSONResult adminFaceLogin(AdminLoginBO adminLoginBO, HttpServletRequest request, HttpServletResponse response) {
        // 0. 判断用户名和人脸信息不能为空
        if(StringUtils.isBlank(adminLoginBO.getUsername())){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_USERNAME_NULL_ERROR);
        }
        String tempFace64 = adminLoginBO.getImg64();
        if (StringUtils.isBlank(tempFace64)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 1. 从数据库中查询出faceId
        AdminUser admin = adminUserService.queryAdminByUsername(adminLoginBO.getUsername());
        String adminFaceId = admin.getFaceId();
        if (StringUtils.isBlank(adminFaceId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_NULL_ERROR);
        }
        // 2. 请求文件服务,获得人懒数据的base64数据
        String fileServerUrlExecute = "http://files.imoocnews.com:8004/fs/readFace64InGridFS?faceId=" + adminFaceId;
        ResponseEntity<GraceJSONResult> responseEntity = restTemplate.getForEntity(fileServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        String base64DB = (String)bodyResult.getData();
        // 3. 调用阿里ai进行人脸对比识别,判断可信度,从而实现人脸登录
        boolean result = faceVerifyUtils.faceVerify(FaceVerifyType.BASE64.type,
                tempFace64,
                base64DB,
                60);
        if (!result){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ADMIN_FACE_LOGIN_ERROR);
        }
        // 4. admin登录后的数据设置,redis与cookie
        doLoginSettings(admin,request,response);
        return GraceJSONResult.ok();
    }
dev-common  com/imooc/utils/FaceVerifyUtils.java
package com.imooc.utils;

import com.aliyuncs.utils.Base64Helper;
import com.imooc.enums.FaceVerifyType;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.extend.AliyunResource;
import org.apache.tomcat.util.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.SimpleTimeZone;

@Component
public class FaceVerifyUtils {

    final static Logger logger = LoggerFactory.getLogger(FaceVerifyUtils.class);

    @Autowired
    private AliyunResource aliyunResource;

    //网关地址
    private static final String gateway = "https://dtplus-cn-shanghai.data.aliyuncs.com/face/verify";

    /*
     * 计算MD5+BASE64
     */
    public static String MD5Base64(String s) {
        if (s == null)
            return null;
        String encodeStr = "";
        byte[] utfBytes = s.getBytes();
        MessageDigest mdTemp;
        try {
            mdTemp = MessageDigest.getInstance("MD5");
            mdTemp.update(utfBytes);
            byte[] md5Bytes = mdTemp.digest();
            Base64Helper b64Encoder = new Base64Helper();
            encodeStr = b64Encoder.encode(md5Bytes);
        } catch (Exception e) {
            throw new Error("Failed to generate MD5 : " + e.getMessage());
        }
        return encodeStr;
    }

    /*
     * 计算 HMAC-SHA1
     */
    public static String HMACSha1(String data, String key) {
        String result;
        try {
            SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA1");
            Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(signingKey);
            byte[] rawHmac = mac.doFinal(data.getBytes());
            result = (new Base64Helper()).encode(rawHmac);
        } catch (Exception e) {
            throw new Error("Failed to generate HMAC : " + e.getMessage());
        }
        return result;
    }

    /*
     * 等同于javaScript中的 new Date().toUTCString();
     */
    public static String toGMTString(Date date) {
        SimpleDateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.UK);
        df.setTimeZone(new SimpleTimeZone(0, "GMT"));
        return df.format(date);
    }

    /**
     * 发送POST请求 进行两张图的人脸对比
     * @param type
     *          0: 通过url识别,参数image_url不为空;1: 通过图片content识别,参数content不为空
     * @param face1
     *          type为0,则传入图片url,为1则传入base64
     * @param face2
     *          type为0,则传入图片url,为1则传入base64
     * @return
     */
    //如果发送的是转换为base64编码后后面加请求参数type为1,如果请求的是图片的url则不用加type参数。
    public String sendPostVerifyFace(int type, String face1, String face2) throws Exception {
        String body = "";
        if (type == FaceVerifyType.BASE64.type) {
            body = "{\"content_1\": \"" + face1 + "\", \"content_2\":\"" + face2 + "\", \"type\":\"" + type + "\"}";
        } else if (type == FaceVerifyType.IMAGE_URL.type) {
            body = "{\"image_url_1\": \"" + face1 + "\", \"image_url_2\":\"" + face2 + "\", \"type\":\"" + type + "\"}";
        } else {
            GraceException.display(ResponseStatusEnum.FACE_VERIFY_TYPE_ERROR);
        }
//        String body = "{\"content_1\": \"" + face1 + "\", \"content_2\":\"" + face2 + "\", \"type\":\"" + "1" + "\"}";
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        int statusCode = 200;
        try {
            URL realUrl = new URL(gateway);
            /*
             * http header 参数
             */
            String method = "POST";
            // 返回值类型
            String accept = "application/json";
            // 请求内容类型
            String content_type = "application/json";
            String path = realUrl.getFile();
            // GMT时间
            String date = toGMTString(new Date());
            // 1.对body做MD5+BASE64加密
            String bodyMd5 = MD5Base64(body);
            String stringToSign = method + "\n" + accept + "\n" + bodyMd5 + "\n" + content_type + "\n" + date + "\n"
                    + path;
            // 2.计算 HMAC-SHA1
            String signature = HMACSha1(stringToSign, aliyunResource.getAccessKeySecret());
            // 3.得到 authorization header
            String authHeader = "Dataplus " + aliyunResource.getAccessKeyID() + ":" + signature;
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("Accept", accept);
            conn.setRequestProperty("Content-type", content_type);
            conn.setRequestProperty("Date", date);
            // 认证信息
            conn.setRequestProperty("Authorization", authHeader);
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            out = new PrintWriter(conn.getOutputStream());
            // 发送请求参数
            out.print(body);
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            statusCode = ((HttpURLConnection) conn).getResponseCode();
            if (statusCode != 200) {
                in = new BufferedReader(new InputStreamReader(((HttpURLConnection) conn).getErrorStream()));
            } else {
                in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            }
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        if (statusCode != 200) {
            throw new IOException("\nHttp StatusCode: " + statusCode + "\nErrorMessage: " + result);
        }
        return result;
    }

    /**
     *
     * @param type
     * @param face1
     * @param face2
     * @param targetConfidence
     *          目标可信度,自定义阈值
     * @return
     */
    public boolean faceVerify(int type, String face1, String face2, double targetConfidence) {

        String response = null;
        try {
            response = sendPostVerifyFace(type, face1, face2);
        } catch (Exception e) {
            e.printStackTrace();
        }

        Map<String, String> map = JsonUtils.jsonToPojo(response, Map.class);
        Object confidenceStr = map.get("confidence");
        Double responseConfidence = (Double)confidenceStr;

        logger.info("人脸对比结果:{}", responseConfidence);

//        System.out.println(response.toString());
//        System.out.println(map.toString());

        if (responseConfidence > targetConfidence) {
            return true;
        } else {
            return false;
        }
    }

    /**
     *
     * 将图片转换为Base64
     * 将base64编码字符串解码成img图片
     * @param imgUrl
     * @return
     */
    public String getImgBase64(String imgUrl){
        ByteArrayOutputStream data = new ByteArrayOutputStream();
        try {
            // 创建URL
            URL url = new URL(imgUrl);
            byte[] by = new byte[1024];
            // 创建链接
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5000);
            InputStream is = conn.getInputStream();
            // 将内容放到内存中
            int len = -1;
            while ((len = is.read(by)) != -1) {
                data.write(by, 0, len);
            }
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 对字节数组Base64编码
        return Base64.encodeBase64String(data.toByteArray());
    }

//    public static void main(String[] args) {
//        String face3 = "http://122.152.205.72:88/group1/M00/00/05/CpoxxF5MvvGAfnLXAAIHiv37wNk363.jpg";
//        String face4 = "http://122.152.205.72:88/group1/M00/00/05/CpoxxF5Mv3yAH74mAACOiTd9pO4462.jpg";
//
//        boolean result = new FaceVerifyUtils().faceVerify(FaceVerifyType.IMAGE_URL.type, face3, face4, 60);
//
//        logger.info("人脸对比是否成功:{}", result);
//    }
}

MongoDB使用场景 【分担数据库的大数据量】

  • GridFS小文件存储
  • 历史数据快照 [买的东西涨价后 还是原来的价格] 【数据量大存入MongoDB】
  • 用户浏览记录
  • 客服聊天记录 [不是核心数据 可以剥离]
这些不建议放在Redis里 因为Redis是存储在内存里的 [内存很贵 成本很大]

友情连接保存与更新 【MongoDB】

对连接的一些逻辑校验
service-admin  application.yml 【加上mongodb配置】
  data:
    mongodb:
      uri: mongodb://root:root@192.168.170.135:27017
      database: imooc-news
service-admin  Application 【注释exclude 把mongodb配置进来】
package com.imooc.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication  //(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.admin.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

dev-model com/imooc/pojo/bo/SaveFriendLinkBO.java
package com.imooc.pojo.bo;

import com.imooc.validate.CheckUrl;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class SaveFriendLinkBO {
private String id;
@NotBlank(message = “友情链接名不能为空”)
private String linkName;
@NotBlank(message = “友情链接地址不能为空”)
@CheckUrl 【ctrl+左键 显示↓ CheckUrl接口】
@CheckName 【 //不能有空格 不能为空 字符串长度要在6-12位】
private String linkUrl;
@NotNull(message = “请选择保留或删除”)
private Integer isDelete;
}Getter+Setter

dev-model  com/imooc/validate/CheckUrl.java
package com.imooc.validate;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckUrlValidate.class)
public @interface CheckUrl {

    String message() default "Url不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
dev-model  com/imooc/validate/CheckName.java
package com.imooc.validate;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckUrlValidate.class)
public @interface CheckName {

    String message() default "Name不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
dev-model  com/imooc/validate/CheckUrlValidate.java
package com.imooc.validate;

import com.imooc.utils.UrlUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CheckUrlValidate implements ConstraintValidator<CheckUrl, String> {

    @Override
    public boolean isValid(String url, ConstraintValidatorContext context) {
        return UrlUtil.verifyUrl(url.trim());
    }
}
dev-model  com/imooc/validate/CheckNameValidate.java
package com.imooc.validate;

import com.imooc.utils.UrlUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CheckNameValidate implements ConstraintValidator<CheckName, String> {

    @Override
    public boolean isValid(String name, ConstraintValidatorContext context) {
        return UrlUtil.verifyName(name.trim());
    }
}
dev-common  com/imooc/utils/UrlUtil.java 【Url+Name校验标准】
package com.imooc.utils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class UrlUtil {

    /**
     * 验证是否是URL
     * @param url
     * @return
     */
    public static boolean verifyUrl(String url){

        // URL验证规则
//        String regEx ="[A-Za-z]+://[A-Za-z0-9-_]+\\\\.[A-Za-z0-9-_%&\\?\\/.=]+";
        String regEx = "^([hH][tT]{2}[pP]:/*|[hH][tT]{2}[pP][sS]:/*|[fF][tT][pP]:/*)(([A-Za-z0-9-~]+).)+([A-Za-z0-9-~\\/])+(\\?{0,1}(([A-Za-z0-9-~]+\\={0,1})([A-Za-z0-9-~]*)\\&{0,1})*)$";
        // 编译正则表达式
        Pattern pattern = Pattern.compile(regEx);
        // 忽略大小写的写法
        // Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(url);
        // 字符串是否与正则表达式相匹配
        boolean rs = matcher.matches();
        return rs;

    }
    //不能有空格 不能为空 字符串长度要在6-12位
    public static boolean verifyName(String name){
        // Name验证规则
        String nameEx = "^[^\\s]{6,12}$";
        // 编译正则表达式
        Pattern pattern = Pattern.compile(nameEx);
        // 忽略大小写的写法
        // Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(name);
        // 字符串是否与正则表达式相匹配
        boolean rs = matcher.matches();
        return rs;
    }

    public static void main(String[] args) {
        boolean res = verifyUrl("http://admin.imoocnews.com:9090/imooc-news/admin/friendLinks.html");
        boolean nres = verifyName("Jerry");
        System.out.println(nres);
    }
}
dev-model  pom.xml
 <!-- 引入 mongodb 依赖 --> 【springboot整合mongodb】
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>2.7.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
真正的友链保存接口
service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "首页友情连接维护", tags = {"首页友情连接维护"})
@RequestMapping("friendLinkMng")
public interface FriendLinkControllerApi {

    @ApiOperation(value = "新增或者修改友情连接", notes = "新增或者修改友情连接", httpMethod = "POST")
    @PostMapping("/saveOrUpdateFriendLink")
    public GraceJSONResult saveOrUpdateFriendLink(@RequestBody SaveFriendLinkBO saveFriendLinkBO,
                                      BindingResult result);
}
service-admin  com/imooc/admin/controller/FriendLinkController.java
package com.imooc.admin.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.FriendLinkControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import com.imooc.pojo.mo.FriendLinkMO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.Map;

@RestController
public class FriendLinkController extends BaseController implements FriendLinkControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FriendLinkController.class);


    @Override
    public GraceJSONResult saveOrUpdateFriendLink(SaveFriendLinkBO saveFriendLinkBO, BindingResult result) {
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }

//      saveFriendLinkBO -> ***Mo  MongoDB校验的对象
        FriendLinkMO friendLinkMO = new FriendLinkMO();
        BeanUtils.copyProperties(saveFriendLinkBO,friendLinkMO);
        friendLinkMO.setCreateTime(new Date());
        friendLinkMO.setUpdateTime(new Date());
        return GraceJSONResult.ok();
    }
}
dev-model  com/imooc/pojo/mo/FriendLinkMO.java
//这些都是设置到MongoDB数据库的名字
//@Document("FriendLink") //MongoDB文件起别名
public class FriendLinkMO {
    @Id //作为MongDB的主键了
    private String id;
    @Field("link_name")
    private String linkName;
    @Field("link_url")
    private String linkUrl;
    @Field("is_delete")
    private Integer isDelete;
    @Field("create_time")
    private Date createTime;
    @Field("update_time")
    private Date updateTime;
}Getter + Setter

Repository持久层操作保存记录

service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "首页友情连接维护", tags = {"首页友情连接维护"})
@RequestMapping("friendLinkMng")
public interface FriendLinkControllerApi {

    @ApiOperation(value = "新增或者修改友情连接", notes = "新增或者修改友情连接", httpMethod = "POST")
    @PostMapping("/saveOrUpdateFriendLink")
    public GraceJSONResult saveOrUpdateFriendLink(@RequestBody SaveFriendLinkBO saveFriendLinkBO,
                                      BindingResult result);
}
service-admin  com/imooc/admin/controller/FriendLinkController.java
package com.imooc.admin.controller;

import com.imooc.admin.repository.FriendLinkRepository;
import com.imooc.admin.service.FriendLinkService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.FriendLinkControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import com.imooc.pojo.mo.FriendLinkMO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.Map;

@RestController
public class FriendLinkController extends BaseController implements FriendLinkControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FriendLinkController.class);
    @Autowired
    private FriendLinkService friendLinkService;

    @Override
    public GraceJSONResult saveOrUpdateFriendLink(SaveFriendLinkBO saveFriendLinkBO, BindingResult result) {
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }

//      saveFriendLinkBO -> ***Mo  MongoDB校验的对象
        FriendLinkMO friendLinkMO = new FriendLinkMO();
        BeanUtils.copyProperties(saveFriendLinkBO,friendLinkMO);
        friendLinkMO.setCreateTime(new Date());
        friendLinkMO.setUpdateTime(new Date());

        friendLinkService.saveOrUpdateFriendLink(friendLinkMO);
        return GraceJSONResult.ok();
    }
}
// http://admin.imoocnews.com:9090/imooc-news/admin/friendLinks.html
/* 友情连接 → 
链接名称:慕课网
链接地址:www.imooc.com
[新增/添加]

打开检查→Console
{"status":200,"msg":"操作成功!","success":true,"data":null}

打开数据库查看MongoDB→friendLinkMO有存入的数据即操作成功
*/
service-admin  com/imooc/admin/service/FriendLinkService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.mo.FriendLinkMO;
import com.imooc.utils.PagedGridResult;

public interface FriendLinkService {
    /**
     * 新增或者更新友情链接
     */
    public void saveOrUpdateFriendLink(FriendLinkMO friendLinkMO);
}
service-admin  com/imooc/admin/service/impl/FriendLinkServiceImpl.java
package com.imooc.admin.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.repository.FriendLinkRepository;
import com.imooc.admin.service.AdminUserService;
import com.imooc.admin.service.FriendLinkService;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.mo.FriendLinkMO;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class FriendLinkServiceImpl implements FriendLinkService {

    @Autowired
    private FriendLinkRepository friendLinkRepository;
    @Override
    public void saveOrUpdateFriendLink(FriendLinkMO friendLinkMO) {
        friendLinkRepository.save(friendLinkMO); //有id更新 无id直接保存
    }
}
service-admin  com/imooc/admin/repository/FriendLinkRepository.java
package com.imooc.admin.repository;

import com.imooc.pojo.mo.FriendLinkMO;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface FriendLinkRepository extends MongoRepository<FriendLinkMO, String> { //持久层
    // 内置提供了很多方法 find.. delete...
}

友情链接查询列表 【MongoDB】

@Document(“FriendLink”) //文件起别名 记得要在MongoDB里面找这个 下面搜索的都在这个文件里面
service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.AdminLoginBO;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Api(value = "首页友情连接维护", tags = {"首页友情连接维护"})
@RequestMapping("friendLinkMng")
public interface FriendLinkControllerApi {

    @ApiOperation(value = "新增或者修改友情连接", notes = "新增或者修改友情连接", httpMethod = "POST")
    @PostMapping("/saveOrUpdateFriendLink")
    public GraceJSONResult saveOrUpdateFriendLink(@RequestBody SaveFriendLinkBO saveFriendLinkBO,
                                      BindingResult result);
    @ApiOperation(value = "查询改友情连接列表", notes = "查询改友情连接列表", httpMethod = "POST")
    @PostMapping("/getFriendLinkList")
    public GraceJSONResult getFriendLinkList();
}
service-admin  com/imooc/admin/controller/FriendLinkController.java
package com.imooc.admin.controller;

import com.imooc.admin.repository.FriendLinkRepository;
import com.imooc.admin.service.FriendLinkService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.FriendLinkControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveFriendLinkBO;
import com.imooc.pojo.mo.FriendLinkMO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.Map;

@RestController
public class FriendLinkController extends BaseController implements FriendLinkControllerApi {
    final static Logger logger = LoggerFactory.getLogger(FriendLinkController.class);
    @Autowired
    private FriendLinkService friendLinkService;

    @Override
    public GraceJSONResult saveOrUpdateFriendLink(SaveFriendLinkBO saveFriendLinkBO, BindingResult result) {
        if (result.hasErrors()){
            Map<String, String> map = getErrors(result);
            return GraceJSONResult.errorMap(map);
        }

//      saveFriendLinkBO -> ***Mo  MongoDB校验的对象
        FriendLinkMO friendLinkMO = new FriendLinkMO();
        BeanUtils.copyProperties(saveFriendLinkBO,friendLinkMO);
        friendLinkMO.setCreateTime(new Date());
        friendLinkMO.setUpdateTime(new Date());

        friendLinkService.saveOrUpdateFriendLink(friendLinkMO);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult getFriendLinkList() { 
//【用了FriendLinkRepository里面的】extends MongoRepository 中的简单增删改查 
// 里面的删除是逻辑删除
        return GraceJSONResult.ok(friendLinkService.queryAllFriendLinkList());
    }
}
// http://admin.imoocnews.com:9090/imooc-news/admin/friendLinks.html
service-admin  com/imooc/admin/service/FriendLinkService.java
package com.imooc.admin.service;

import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.mo.FriendLinkMO;
import com.imooc.utils.PagedGridResult;

import java.util.List;

public interface FriendLinkService {
    /**
     * 新增或者更新友情链接
     */
    public void saveOrUpdateFriendLink(FriendLinkMO friendLinkMO);

    /**
     * 查询友情链接
     */
    public List<FriendLinkMO> queryAllFriendLinkList();

}
service-admin  com/imooc/admin/service/impl/FriendLinkServiceImpl.java
package com.imooc.admin.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.imooc.admin.mapper.AdminUserMapper;
import com.imooc.admin.repository.FriendLinkRepository;
import com.imooc.admin.service.AdminUserService;
import com.imooc.admin.service.FriendLinkService;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AdminUser;
import com.imooc.pojo.bo.NewAdminBO;
import com.imooc.pojo.mo.FriendLinkMO;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class FriendLinkServiceImpl implements FriendLinkService {

    @Autowired
    private FriendLinkRepository friendLinkRepository;
    @Override
    public void saveOrUpdateFriendLink(FriendLinkMO friendLinkMO) {
        friendLinkRepository.save(friendLinkMO); //有id更新 无id直接保存
    }

    @Override
    public List<FriendLinkMO> queryAllFriendLinkList() {
//        Pageable pageable = PageRequest.of(1,10);
//        friendLinkRepository.findAll(pageable);
        return friendLinkRepository.findAll();
    }
}

友情链接删除 【MongoDB】[增加真实删除]

service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
...
/*
@Api(value = "首页友情连接维护", tags = {"首页友情连接维护"})
@RequestMapping("friendLinkMng")
public interface FriendLinkControllerApi {

    @ApiOperation(value = "新增或者修改友情连接", notes = "新增或者修改友情连接", httpMethod = "POST")
    @PostMapping("/saveOrUpdateFriendLink")
    public GraceJSONResult saveOrUpdateFriendLink(@RequestBody SaveFriendLinkBO saveFriendLinkBO,
                                      BindingResult result);
    @ApiOperation(value = "查询改友情连接列表", notes = "查询改友情连接列表", httpMethod = "POST")
    @PostMapping("/getFriendLinkList")
    public GraceJSONResult getFriendLinkList();
*/
    @ApiOperation(value = "删除改友情连接列表", notes = "删除改友情连接列表", httpMethod = "POST")
    @PostMapping("/delete")
    public GraceJSONResult delete(@RequestParam String linkId);
}
service-admin  com/imooc/admin/controller/FriendLinkController.java
@Override
    public GraceJSONResult delete(String linkId) {
        friendLinkService.delete(linkId);
        return GraceJSONResult.ok();
    }
service-admin  com/imooc/admin/service/FriendLinkService.java
    /**
     * 删除友情链接
     */
    public void delete(String linkId);
service-admin  com/imooc/admin/service/AdminUserService.java
@Override
    public void delete(String linkId) {
        friendLinkRepository.deleteById(linkId);
    }
service-api  com/imooc/api/config/InterceptorConfig.java 【增加友链拦截器】
/*
package com.imooc.api.config;

import com.imooc.api.interceptors.AdminTokenInterceptor;
import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UserActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UserActiveInterceptor userActiveInterceptor() {
        return new UserActiveInterceptor();
    }

    @Bean
    public AdminTokenInterceptor adminTokenInterceptor() {
        return new AdminTokenInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace");
        registry.addInterceptor(adminTokenInterceptor())//继续添加拦截器:查询admin列表 创建新admin用户
                .addPathPatterns("/adminMng/adminIsExist")
                .addPathPatterns("/adminMng/addNewAdmin")
                .addPathPatterns("/adminMng/getAdminList")
                .addPathPatterns("/fs/uploadToGridFS")
*/
                .addPathPatterns("/friendLinkMng/saveOrUpdateFriendLink")
                .addPathPatterns("/friendLinkMng/getFriendLinkList")
                .addPathPatterns("/friendLinkMng/delete");
    }
}

【作业】文章分类管理 [新增或修改分类、查询分类列表、用户端查询分类列表]

http://admin.imoocnews.com:9090/imooc-news/admin/categoryMng.html

@RequestBody的使用-CSDN博客

service-api  com/imooc/api/controller/admin/CategoryMngControllerApi.java
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveCatrgoryBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@Api(value = "文章分类维护", tags = {"文章分类维护controller"})
@RequestMapping("categoryMng")
public interface CategoryMngControllerApi {
    @PostMapping("saveOrUpdateCategory")
    @ApiOperation(value = "新增或修改分类", notes = "新增或修改分类", httpMethod = "POST")
    public GraceJSONResult saveOrUpdateCategory(@RequestBody @Valid SaveCatrgoryBO saveCatrgoryBO,
                                                BindingResult result);
    @PostMapping("getCatList")
    @ApiOperation(value = "查询分类列表", notes = "查询分类列表", httpMethod = "POST")
    public GraceJSONResult getCatList();

    @GetMapping("getCats")
    @ApiOperation(value = "用户端查询分类列表", notes = "用户端查询分类列表", httpMethod = "GET")
    public GraceJSONResult getCats();
}
service-admin  com/imooc/admin/controller/CategoryMngController.java
package com.imooc.admin.controller;

import com.imooc.admin.service.CategoryService;
import com.imooc.api.BaseController;
import com.imooc.api.controller.admin.CategoryMngControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.Category;
import com.imooc.pojo.bo.SaveCatrgoryBO;
import com.imooc.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
public class CategoryMngController extends BaseController implements CategoryMngControllerApi {

    final static Logger logger = LoggerFactory.getLogger(CategoryMngController.class);

    @Autowired
    private CategoryService categoryService;

    @Override
    public GraceJSONResult saveOrUpdateCategory(SaveCatrgoryBO saveCatrgoryBO, BindingResult result) {
        if (result.hasErrors()){
        // 判断BindingResult是否保存错误的验证信息,如果有,则直接return
            Map<String, String> errorMap = getErrors(result);
            return GraceJSONResult.errorMap(errorMap);
        }
        Category newCat = new Category();
        BeanUtils.copyProperties(saveCatrgoryBO,newCat);
        // id为空新增,不为空修改
        if (saveCatrgoryBO.getId() == null){
            //查询新增的分类名称不能重复存在
            boolean isExist = categoryService.queryCatIsExist(newCat.getName(), null);
            if (!isExist){
                //新增到数据库
                categoryService.createCategory(newCat);
            }else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.CATEGORY_EXIST_ERROR);
            }
        }else {
            //查询修改的分类名称不能重复存在
            boolean isExist = categoryService.queryCatIsExist(newCat.getName(), saveCatrgoryBO.getOldName());
            if (!isExist){
                //修改到数据库
                categoryService.modifyCategory(newCat);
            } else {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.CATEGORY_EXIST_ERROR);
            }
        }
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult getCatList() {
        List<Category> categoryList = categoryService.queryCategoryList();
        return GraceJSONResult.ok(categoryList);
    }

    @Override
    public GraceJSONResult getCats() {
        // 先从redis中查询,如果有,则返回,如果没有,则查询数据库库后先放缓存,放返回
        String allCatJson = redis.get(REDIS_ALL_CATEGORY);

        List<Category> categoryList = null;
        if (StringUtils.isBlank(allCatJson)) {
            categoryList = categoryService.queryCategoryList();
            redis.set(REDIS_ALL_CATEGORY, JsonUtils.objectToJson(categoryList));
        } else {
            categoryList = JsonUtils.jsonToList(allCatJson, Category.class);
        }

        return GraceJSONResult.ok(categoryList);
    }
}
service-admin  com/imooc/admin/service/CategoryService.java
package com.imooc.admin.service;

import com.imooc.pojo.Category;
import java.util.List;

public interface CategoryService {

    /**
     * 新增文章分类
     */
    public void createCategory(Category category);

    /**
     * 修改文章分类列表
     */
    public void modifyCategory(Category category);

    /**
     * 查询分类名是否已经存在
     */
    public boolean queryCatIsExist(String catName, String oldCatName);

    /**
     * 获得文章分类列表
     */
    public List<Category> queryCategoryList();
}
service-admin  com/imooc/admin/service/impl/CategoryServiceImpl.java
package com.imooc.admin.service.impl;

import com.imooc.admin.mapper.CategoryMapper;
import com.imooc.admin.service.CategoryService;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.Category;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.List;

import static com.imooc.api.BaseController.REDIS_ALL_CATEGORY;


@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
    public CategoryMapper categoryMapper;
    @Autowired
    public RedisOperator redis;

    @Transactional
    @Override
    public void createCategory(Category category) {
// 分类不会很多,所以id不需要自增,这个表的数据也不会多到几万甚至分表,数据都会集中在一起
        int result = categoryMapper.insert(category);
        if (result != 1){
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
            /**
             * 不建议如下做法:
             * 1. 查询redis中的categoryList
             * 2. 转化categoryList为list类型
             * 3. 在categoryList中add一个当前的category
             * 4. 再次转换categoryList为json,并存入redis中
             */
            // 直接使用redis删除缓存即可,用户端在查询的时候会直接查库,再把最新的数据放入到缓存中
            redis.del(REDIS_ALL_CATEGORY);
        }
    }

    @Transactional
    @Override
    public void modifyCategory(Category category) {
        int result = categoryMapper.updateByPrimaryKey(category);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
        }
        // 直接使用redis删除缓存即可,用户端在查询的时候会直接查库,再把最新的数据放入到缓存中
        redis.del(REDIS_ALL_CATEGORY);
    }

    @Override
    public boolean queryCatIsExist(String catName, String oldCatName) {
        Example example = new Example(Category.class);
        Example.Criteria catCriteria = example.createCriteria();
        catCriteria.andEqualTo("name", catName);
        if (StringUtils.isNotBlank(oldCatName)) {
            catCriteria.andNotEqualTo("name", oldCatName);
        }

        List<Category> catList = categoryMapper.selectByExample(example);

        boolean isExist = false;
        if (catList != null && !catList.isEmpty() && catList.size() > 0) {
            isExist = true;
        }

        return isExist;
    }

    @Override
    public List<Category> queryCategoryList() {
        return categoryMapper.selectAll();
    }
}
service-admin  com/imooc/admin/mapper/CategoryMapper.java
package com.imooc.admin.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Category;
import org.springframework.stereotype.Repository;

@Repository
public interface CategoryMapper extends MyMapper<Category> {
}
service-admin  resources/mapper/CategoryMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.admin.mapper.CategoryMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Category" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="name" property="name" jdbcType="VARCHAR" />
    <result column="tag_color" property="tagColor" jdbcType="VARCHAR" />
  </resultMap>
</mapper>
service-api  com/imooc/api/BaseController.java
public abstract class BaseController {
    @Autowired
    public RedisOperator redis;
    public static final String MOBILE_SMSCODE = "mobile:smscode";
    public static final String REDIS_USER_TOKEN = "redis_user_token";//ctrl+shift+u直接大写
    public static final String REDIS_USER_INFO = "redis_user_info";//ctrl+shift+u直接大写
    public static final String REDIS_ADMIN_TOKEN = "redis_admin_token";//ctrl+shift+u直接大写
    public static final String REDIS_ALL_CATEGORY = "redis_all_category";

    public static final String REDIS_WRITER_FANS_COUNTS = "redis_writer_fans_counts";
    public static final String REDIS_MY_FOLLOW_COUNTS = "redis_my_follow_counts";

    public static final String REDIS_ARTICLE_READ_COUNTS = "redis_article_read_counts";
    public static final String REDIS_ALREADY_READ = "redis_already_read";

    public static final String REDIS_ARTICLE_COMMENT_COUNTS = "redis_article_comment_counts";

    @Value("${website.domain-name}")
    public String DOMAIN_NAME;
    public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;
    public static final Integer COOKIE_DELETE = 0;

    public static final Integer COMMON_START_PAGE = 1;
    public static final Integer COMMON_PAGE_SIZE = 10;
}...
dev-model  com/imooc/pojo/Category.java
package com.imooc.pojo;

import javax.persistence.Column;
import javax.persistence.Id;

public class Category {
    @Id
    private Integer id;

    /**
     * 分类名,比如:科技,人文,历史,汽车等等
     */
    private String name;

    /**
     * 标签颜色
     */
    @Column(name = "tag_color")
    private String tagColor;
}Getter + Setter

查询用户列表_设置时间日期转换配置 【用户管理】

service-api  com/imooc/api/controller/user/AppUserMngControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Date;

@Api(value = "用户管理相关的接口定义",tags = {"用户管理相关功能的controller"})
@RequestMapping("appUser")
public interface AppUserMngControllerApi {
    @ApiOperation(value = "查询所有网站用户",notes = "查询所有网站用户",httpMethod = "POST")
    @PostMapping("queryAll")
    public GraceJSONResult queryAll(@RequestParam String nickname,
                                    @RequestParam Integer status,
                                    @RequestParam Date startDate,
                                    @RequestParam Date endDate,
                                    @RequestParam Integer page,
                                    @RequestParam Integer pageSize);
}
service-user  com/imooc/user/controller/AppUserMngController.java
package com.imooc.user.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.user.AppUserMngControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
public class AppUserMngController extends BaseController implements AppUserMngControllerApi {
    final static Logger logger = LoggerFactory.getLogger(AppUserMngController.class);
// 字符串无法直接转换成Date类型 需要工具类转换 DateConverterConfig com/imooc/api/config/DateConverterConfig.java

    @Override
    public GraceJSONResult queryAll(String nickname, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize) {
        System.out.println(startDate);
        System.out.println(endDate);
        if (page == null){
            page = COMMON_START_PAGE;
        }
        if (pageSize == null){
            pageSize = COMMON_PAGE_SIZE;
        }

        return GraceJSONResult.ok();
    }
}
// http://admin.imoocnews.com:9090/imooc-news/admin/userList.html
service-api  com/imooc/api/config/DateConverterConfig.java
package com.imooc.api.config;


import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 请求路径url中的参数进行时间日期类型的转换,字符串->日期Date
 */
@Configuration
public class DateConverterConfig implements Converter<String, Date> {

    private static final List<String> formatterList = new ArrayList<>(4);
    static{
        formatterList.add("yyyy-MM");
        formatterList.add("yyyy-MM-dd");
        formatterList.add("yyyy-MM-dd hh:mm");
        formatterList.add("yyyy-MM-dd hh:mm:ss");
    }

    @Override
    public Date convert(String source) {
        String value = source.trim();
        if ("".equals(value)) {
            return null;
        }
        if(source.matches("^\\d{4}-\\d{1,2}$")){
            return parseDate(source, formatterList.get(0));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")){
            return parseDate(source, formatterList.get(1));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")){
            return parseDate(source, formatterList.get(2));
        }else if(source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")){
            return parseDate(source, formatterList.get(3));
        }else {
            GraceException.display(ResponseStatusEnum.SYSTEM_DATE_PARSER_ERROR);
        }
        return null;
    }

    /**
     * 日期转换方法
     * @param dateStr
     * @param formatter
     * @return
     */
    public Date parseDate(String dateStr, String formatter) {
        Date date=null;
        try {
            DateFormat dateFormat = new SimpleDateFormat(formatter);
            date = dateFormat.parse(dateStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return date;
    }
}

查询用户列表_实现service与联调 【用户管理】

service-user  com/imooc/user/service/AppUserMngService.java
package com.imooc.user.service;

import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;

public interface AppUserMngService {
    /**
     * 查询管理员列表
     * @param nickname
     * @param status
     * @param startDate
     * @param endDate
     * @param page
     * @param pageSize
     * @return
     */
    public PagedGridResult queryAllUserList(String nickname, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize);

}
service-api  com/imooc/api/service/BaseService.java
package com.imooc.api.service;

import com.github.pagehelper.PageInfo;
import com.imooc.utils.PagedGridResult;

import java.util.List;

public class BaseService {
    public PagedGridResult setterPagedGrid(List<?> list, Integer page){ //类型是? 后期不确定是什么泛型
        PageInfo<?> pageList = new PageInfo<>(list);
        PagedGridResult gridResult = new PagedGridResult();
        gridResult.setRows(list);
        gridResult.setPage(page);
        gridResult.setRecords(pageList.getTotal());
        gridResult.setTotal(pageList.getPages());
        return gridResult;

    }
}
service-user  com/imooc/user/service/impl/AppUserMngServiceImpl.java
package com.imooc.user.service.impl;

import com.github.pagehelper.PageHelper;
import com.imooc.api.service.BaseService;
import com.imooc.enums.Sex;
import com.imooc.enums.UserStatus;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.service.AppUserMngService;
import com.imooc.user.service.UserService;
import com.imooc.utils.*;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class AppUserMngServiceImpl extends BaseService implements AppUserMngService {
    @Autowired
    public AppUserMapper appUserMapper;

    @Override
    public PagedGridResult queryAllUserList(String nickname, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize) {
        Example example = new Example(AppUser.class);
        example.orderBy("createdTime").desc();
        Example.Criteria criteria = example.createCriteria();
        if (StringUtils.isNotBlank(nickname)) {
            criteria.andLike("nickname", "%" + nickname + "%");
        }
        if (UserStatus.isUserStatusValid(status)){
            criteria.andEqualTo("activeStatus", status); //对比状态
        }
        if (startDate != null){
            criteria.andGreaterThanOrEqualTo("createdTime", startDate);//数据库和传入参数对比
        }
        if (endDate != null){
            criteria.andLessThanOrEqualTo("endTime", endDate);//数据库和传入参数对比
        }
        PageHelper.startPage(page, pageSize);
        List<AppUser> list = appUserMapper.selectByExample(example);

        return setterPagedGrid(list,page);
    }
}

查询用户账户_冻结与解封 【用户管理】

service-api  com/imooc/api/controller/user/AppUserMngControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Date;

@Api(value = "用户管理相关的接口定义",tags = {"用户管理相关功能的controller"})
@RequestMapping("appUser")
public interface AppUserMngControllerApi {
    @ApiOperation(value = "查询所有网站用户",notes = "查询所有网站用户",httpMethod = "POST")
    @PostMapping("queryAll")
    public GraceJSONResult queryAll(@RequestParam String nickname,
                                    @RequestParam Integer status,
                                    @RequestParam Date startDate,
                                    @RequestParam Date endDate,
                                    @RequestParam Integer page,
                                    @RequestParam Integer pageSize);

    @ApiOperation(value = "查看用户详情",notes = "查看用户详情",httpMethod = "POST")
    @PostMapping("userDetail")
    public GraceJSONResult userDetail(@RequestParam String userId);

    @ApiOperation(value = "冻结用户或者解冻用户",notes = "冻结用户或者解冻用户",httpMethod = "POST")
    @PostMapping("freezeUserOrNot")
    public GraceJSONResult freezeUserOrNot(@RequestParam String userId,@RequestParam Integer doStatus);
}
service-user  com/imooc/user/controller/AppUserMngController.java
@RestController
public class AppUserMngController extends BaseController implements AppUserMngControllerApi {
    final static Logger logger = LoggerFactory.getLogger(AppUserMngController.class);
    
    @Autowired
    private AppUserMngService appUserMngService;
    @Autowired
    private UserService userService;
......
    
     @Override
    public GraceJSONResult freezeUserOrNot(String userId, Integer doStatus) {
        if (!UserStatus.isUserStatusValid(doStatus)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_STATUS_ERROR);
        }
        appUserMngService.freezeUserOrNot(userId, doStatus);
        //若冻结后 用户处于登录状态 还可以进行操作 所以要刷新用户状态
        //方法①:删除用户会话,从而保证用户需要重新登陆以后再来刷新她的会话状态
        redis.del(REDIS_USER_INFO + ":" + userId);
        //方法②:查询最新用户的信息,重新放入redis中,做一次更新
        return GraceJSONResult.ok();
    }
service-user  com/imooc/user/service/AppUserMngService.java
package com.imooc.user.service;

import com.imooc.pojo.AppUser;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;

public interface AppUserMngService {
    /**
     * 查询管理员列表
     */
    public PagedGridResult queryAllUserList(String nickname, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize);

    /**
     * 冻结用户账号或者解除冻结
     */
    public void freezeUserOrNot(String userId, Integer doStatus);

}
service-user  com/imooc/user/service/impl/AppUserMngServiceImpl.java
    @Transactional
    @Override
    public void freezeUserOrNot(String userId, Integer doStatus) {
        AppUser user = new AppUser();
        user.setId(userId);
        user.setActiveStatus(doStatus);
        appUserMapper.updateByPrimaryKeySelective(user);
    }

梳理文章article表结构 【文章服务】

  • 构建文章服务
  • 作者中心发表文章
  • 作者中心内容管理
  • 自动审核[阿里客户端],手动审核

构建文章服务工程 【文章服务】

新创建一个Module
GroupId:com.imooc
ArtifactId:imooc-news-dev-service-article
pom参考service-admin移植 resources里的所有文件(除mapper)也要移植

service-article  pom.xml
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>imooc-news-dev</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>imooc-news-dev-service-article</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>
resources logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-article"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-article"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/service-article.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
service-article  com/imooc/article/Application.java
package com.imooc.article;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-article  com/imooc/article/controller/HelloController.java
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    public Object hello() {
        return GraceJSONResult.ok();
    }
}

========================================================================
http://localhost:8001/hello
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": null
}
service-article  application-dev
server:
  port: 8001

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
## setup CN from java, This is resource
website:
  domain-name: imoocnews.com

## open mybatis log in dev
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
############################################################
#
# admin用户微服务
# web访问端口号  约定:8001
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-article
  datasource: # 数据源的相关配置
    type: com.zaxxer.hikari.HikariDataSource          # 数据源类型:HikariCP
    driver-class-name: org.mariadb.jdbc.Driver       # mysql驱动
    url: jdbc:mysql://localhost:3306/imooc-news-dev?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&sessionVariables=tx_isolation='READ-COMMITTED'
    username: root
    password: root
    hikari:
      connection-timeout: 30000       # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 默认:30秒
      minimum-idle: 5                 # 最小连接数
      maximum-pool-size: 20           # 最大连接数
      auto-commit: true               # 自动提交
      idle-timeout: 600000            # 连接超时的最大时长(毫秒),超时则被释放(retired),默认:10分钟
      pool-name: DateSourceHikariCP     # 连接池名字
      max-lifetime: 1800000           # 连接的生命时长(毫秒),超时而且没被使用则被释放(retired),默认:30分钟 1800000ms
      connection-test-query: SELECT 1
      data-source-properties:
        tx_isolation: 'READ-COMMITTED'
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

  data:
    mongodb:
      uri: mongodb://root:root@192.168.170.135:27017
      database: imooc-news
############################################################
#
# mybatis 配置
#
############################################################
mybatis:
  type-aliases-package: com.imooc.pojo          # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml      # mapper映射文件

############################################################
#
# mybatis mapper 配置
#
############################################################
# 通用 Mapper 配置
mapper:
  mappers: com.imooc.my.mapper.MyMapper
  not-empty: false    # 在进行数据库操作的的时候,判断表达式 username != null, 是否追加 username != ''
  identity: MYSQL
# 分页插件配置
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true

summernote与多文件上传需求 【发头条】

https://summernote.org/

【前端工程里面的】createArticle.html
...
<script src="libs/vue.min.js"></script>
<script src="libs/axios.min.js"></script>

<link href="./libs/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="./libs/jquery-3.4.1.min.js"></script>
<script src="libs/layDate-v5.0.9/laydate/laydate.js"></script>
<script src="./libs/bootstrap/js/bootstrap.min.js"></script>
<link href="./libs/summernote/dist/summernote.css" rel="stylesheet">
<script src="./libs/summernote/dist/summernote.js"></script>
<!-- 中文汉化 -->
<script src="libs/summernote/lang/summernote-zh-CN.js"></script>
<script src="js/app.js"></script>

<script type="text/javascript">

......
<!-- 富文本编辑器 -->
        <div id="editor2" class="editor-container">

            <div class="article-title-wrapper">
                <input id="title" class="article-title" placeholder="请输入文字标题(6-30长度)" v-model="articleTitle" maxlength="30"/>
            </div>

            <div class="article-content-wrapper">
                <div id="summernote" class="summernote"></div>
            </div>

            <div class="other-info">
                <div class="cover-wrapper">
                    <div class="cover">文章领域</div>
                    <div class="choose-type">
                        <!-- <select v-model="articleCategory">
                            <option value="0">请选择</option>
                            <option value="1">汽车</option>
                            <option value="2">科技</option>
                            <option value="3">历史</option>
                        </select> -->

                        <select v-model="articleCategory">
                            <option :value="cat.id" v-for="(cat, index) in catList" v-key="index">{{cat.name}}</option>
                        </select>
                        
                    </div>
                </div>
                <div class="cover-wrapper">
                    <div class="cover">文章封面</div>
                    <div class="choose-type">
                        <div><input type="radio" name="articleType" v-model="articleType" value="1" checked/><span class="choose-words">单封面</span></div>
                        <div style="margin-left: 30px;"><input type="radio" v-model="articleType" value="2" name="articleType"/><span class="choose-words">无封面</span></div>
                    </div>
                </div>
                <div class="cover-wrapper" v-show="articleType==1">
                    <div class="cover"></div>
                    <div class="choose-cover">
                        <div class="uploader-comp">
                            <div id="block-choose" class="block-choose" :style="coverStyle">
                                <img src="./img/icon-go-upload.png" style="width: 20px; height: 20px; align-self: center;" v-show="articleCover == '' || articleCover == null"/>
                            </div>
                            <input type="file" @change="uploadCover" @mouseover="mouseOver" @mouseout="mouseOut" id="inputPic" class="inputPic" accept="image/jpeg,image/jpg,image/png">
                        </div>
                        <div style="margin-top: 10px; color: #9b9d9e;">请上传JPG、JPEG、PNG格式的封面图噢~</div>
                    </div>
                </div>
            </div>

            <div class="publish-bottom">
                <div class="buttons">
                    <button class="white-btn" type="button" @click="goBack">返回</button>
                    <button class="white-btn" type="button" @click="preview">预览</button>
                    <!-- <button class="white-btn" type="button" @click="save">保存草稿</button> -->
                    <!-- FIXME: 计算剩余时间,使用RMQ延时队列,或分布式定时任务 -->
                    <button class="white-btn" type="button" @click="doTiming">{{appointWords}}</button>
                    
                    <input type="text" class="timing-date-picker" placeholder="定时日期" id="choose-date" v-show="isAppoint==1" readonly>

                    <button class="red-btn" type="button" @click="publish">发布文章</button>
                </div>
            </div>
        </div>
    </div>

......
// 初始化编辑器
            $('#summernote').summernote({
                placeholder: '请输入正文...',
                lang: 'zh-CN',
                height: 600,
                width: 800,
                border: 0,
                // disableDragAndDrop: true, // 禁止文件拖放
                toolbar: [
                    ['style', ['style']],
                    ['font', ['bold', 'underline', 'clear']],
                    ['color', ['color']],
                    ['para', ['ul', 'ol', 'paragraph']],
                    ['table', ['table']],
                    ['insert', ['link', 'picture']],
                    ['view', ['fullscreen', 'codeview', 'help']]
                ],

实现多文件上传uploadSomeFiles 【发头条】

媒体号作家中心 | 发文章 (imoocnews.com)

service-api  com/imooc/api/controller/files/FileUploaderControllerApi.java
package com.imooc.api.controller.files;

@Api(value = "文件上传的controller",tags = {"xx功能的Controller"})
@RequestMapping("fs")
public interface FileUploaderControllerApi {
    /**
     * 上传单文件
     * @param userId
     * @param file
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "上传用户头像",notes = "上传用户头像",httpMethod = "POST")
    @PostMapping("/uploadFace")
    public GraceJSONResult uploadFace(@RequestParam String userId, MultipartFile file) throws Exception;

    /**
     * 上传多文件
     * @param userId
     * @param files
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "上传用户头像",notes = "上传用户头像",httpMethod = "POST")
    @PostMapping("/uploadSomeFiles")  //因为前端createArticle.html 178行 multiForm.append('files',f,f.name);
    public GraceJSONResult uploadSomeFiles(@RequestParam String userId, MultipartFile[] files) throws Exception;
......
}
service-file  com/imooc/files/controller/FileUploaderController.java
......
@Override
    public GraceJSONResult uploadSomeFiles(String userId, MultipartFile[] files) throws Exception {
        // 声明一个list,用于存放多个图片的地址路径,返回到前端
        List<String> imageUrlList = new ArrayList<>();
        if (files != null && files.length > 0){
            for (MultipartFile file: files){
                String path = "";
                if (file != null){
                    // 获得文件上传的名称
                    String fileName = file.getOriginalFilename();
                    //判断文件名不能为空
                    if (StringUtils.isNotBlank(fileName)){
                        String fileNameArr[] = fileName.split("\\.");
                        //获得后缀名
                        String suffix = fileNameArr[fileNameArr.length - 1];
                        //防止黑客上传文件攻击服务器 判断后缀符合我们的预定义规范
                        if (!suffix.equalsIgnoreCase("png") &&
                                !suffix.equalsIgnoreCase("jpg") &&
                                !suffix.equalsIgnoreCase("jpeg")
                        ){
                           continue;
                        }
                        // fdfs执行上传     要让外面得以访问 ①需要把内网的环境发布到公网 [内网穿透]  ②路由器端口映射到外网  ③fastdfs安装到公网里
                        // path = uploaderService.uploadFdfs(file, suffix);
                        // OSS执行上传
                        path = uploaderService.uploadOSS(file, userId, suffix);
                    }else {
                        continue;
                    }
                }else {
                    continue;
                }
                String finalPath = "";
                if (StringUtils.isNotBlank(path)){
//            finalPath = fileResource.getHost() + path;
                    finalPath = fileResource.getOssHost() + path;
                    // FIXME: 放入到imageUrlList之前,需要对图片做一次审核 [doAliImageReview]
                    imageUrlList.add(finalPath);
                }  else{
                    continue;
                }
//                return GraceJSONResult.ok(finalPath);
//        return GraceJSONResult.ok(doAliImageReview(finalPath)); //这里加了图片审核咯
            }
        }
        return GraceJSONResult.ok(imageUrlList);
    }
......
service-api  com/imooc/api/config/InterceptorConfig.java 
package com.imooc.api.config;
//【增加拦截uploadSomeFiles】
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UserActiveInterceptor userActiveInterceptor() {
        return new UserActiveInterceptor();
    }

    @Bean
    public AdminTokenInterceptor adminTokenInterceptor() {
        return new AdminTokenInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace")
                .addPathPatterns("/fs/uploadSomeFiles");

        registry.addInterceptor(adminTokenInterceptor())//继续添加拦截器:查询admin列表 创建新admin用户
                .addPathPatterns("/adminMng/adminIsExist")
                .addPathPatterns("/adminMng/addNewAdmin")
                .addPathPatterns("/adminMng/getAdminList")
                .addPathPatterns("/fs/uploadToGridFS")
                .addPathPatterns("/friendLinkMng/saveOrUpdateFriendLink")
                .addPathPatterns("/friendLinkMng/getFriendLinkList")
                .addPathPatterns("/friendLinkMng/delete")
                .addPathPatterns("/categoryMng/saveOrUpdateCategory")
                .addPathPatterns("/categoryMng/getCatList");

        registry.addInterceptor(userActiveInterceptor())
                .addPathPatterns("/fs/uploadSomeFiles");
    }
}

获得列表_业务接口解耦与Redis缓存应用 【文章领域】

getCatList 和 getCats 一个是用户端一个是admin 业务体系不一样 所以同样是查询分类列表
但是还是应该拆开 使耦合减少 得到高效解耦
查询放在Redis里面 效率变高

媒体号作家中心 | 发文章 (imoocnews.com)
刷新一下 文章领域就可以找到那些分类
Redis里面会有信息 redis_all_category
[{“id”:2,”name”:”汽车”,”tagColor”:”#8939bd”},{“id”:3,”name”:”娱乐”,”tagColor”:”#c939aa”},{“id”:5,”name”:”地理”,”tagColor”:”#57394a”},{“id”:6,”name”:”历史”,”tagColor”:”#29ab4a”},{“id”:7,”name”:”科技”,”tagColor”:”#2467bc”},{“id”:9,”name”:”体育”,”tagColor”:”#c98f4a”},{“id”:10,”name”:”搞笑”,”tagColor”:”#68b84a”},{“id”:11,”name”:”技术”,”tagColor”:”#c9394a”},{“id”:12,”name”:”慕课”,”tagColor”:”#682aa8”},{“id”:13,”name”:”技能”,”tagColor”:”#c9394a”},{“id”:14,”name”:”课网”,”tagColor”:”#c9a24a”}]

service-api  com/imooc/api/controller/admin/CategoryMngControllerApi.java
// 【getCasts】
package com.imooc.api.controller.admin;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.SaveCatrgoryBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@Api(value = "文章分类维护", tags = {"文章分类维护controller"})
@RequestMapping("categoryMng")
public interface CategoryMngControllerApi {
    @PostMapping("saveOrUpdateCategory")
    @ApiOperation(value = "新增或修改分类", notes = "新增或修改分类", httpMethod = "POST")
    public GraceJSONResult saveOrUpdateCategory(@RequestBody @Valid SaveCatrgoryBO saveCatrgoryBO,
                                                BindingResult result);
    @PostMapping("getCatList")
    @ApiOperation(value = "查询分类列表", notes = "查询分类列表", httpMethod = "POST")
    public GraceJSONResult getCatList();

    @GetMapping("getCats")
    @ApiOperation(value = "用户端查询分类列表", notes = "用户端查询分类列表", httpMethod = "GET")
    public GraceJSONResult getCats();
}
service-admin  com/imooc/admin/controller/CategoryMngController.java
@Override
    public GraceJSONResult getCats() {
        // 先从redis中查询,如果有,则返回,如果没有,则查询数据库库后先放缓存,放返回
        String allCatJson = redis.get(REDIS_ALL_CATEGORY);

        List<Category> categoryList = null;
        if (StringUtils.isBlank(allCatJson)) {
            categoryList = categoryService.queryCategoryList();
            redis.set(REDIS_ALL_CATEGORY, JsonUtils.objectToJson(categoryList));
        } else {
            categoryList = JsonUtils.jsonToList(allCatJson, Category.class);
        }

        return GraceJSONResult.ok(categoryList);
    }

admin端维护数据缓存 【文章领域】

文章分类 | 运营管理平台 (imoocnews.com)
在管理员修改文章类型后 【课网 → 课课】
媒体号作家中心 | 发文章 (imoocnews.com)
回到用户发文章的文章领域类型也会一起修改
慕课新闻 | 风间影月 (imoocnews.com)
同时首页上方的栏目框也会修改

service-admin  com/imooc/admin/service/impl/CategoryServiceImpl.java
@Service
public class CategoryServiceImpl extends BaseService implements CategoryService {
    @Autowired
    public CategoryMapper categoryMapper;

    @Transactional
    @Override
    public void createCategory(Category category) {
// 分类不会很多,所以id不需要自增,这个表的数据也不会多到几万甚至分表,数据都会集中在一起
        int result = categoryMapper.insert(category);
        if (result != 1){
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
            /**
             * 不建议如下做法:
             * 1. 查询redis中的categoryList
             * 2. 转化categoryList为list类型
             * 3. 在categoryList中add一个当前的category
             * 4. 再次转换categoryList为json,并存入redis中
             */
            // 直接使用redis删除缓存即可,用户端在查询的时候会直接查库,再把最新的数据放入到缓存中
            redis.del(REDIS_ALL_CATEGORY);
        }
    }

    @Transactional
    @Override
    public void modifyCategory(Category category) {
        int result = categoryMapper.updateByPrimaryKey(category);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
        }
        // 直接使用redis删除缓存即可,用户端在查询的时候会直接查库,再把最新的数据放入到缓存中
        redis.del(REDIS_ALL_CATEGORY);
    }
......

发布文章入库Controller及验证【发头条】

service-api  com/imooc/api/controller/article/ArticleControllerApi.java
package com.imooc.api.controller.article;

@Api(value = "文章业务的controller", tags = {"文章业务的controller"})
@RequestMapping("article")
public interface ArticleControllerApi {
    @PostMapping("createArticle")
    @ApiOperation(value = "用户发文", notes = "用户发文", httpMethod = "POST")
    public GraceJSONResult createArticle(@RequestBody @Valid NewArticleBO newArticleBO, BindingResult result);
}
service-article  com/imooc/article/controller/ArticleController.java
package com.imooc.article.controller;

@RestController
public class ArticleController extends BaseController implements ArticleControllerApi {
    final static Logger logger = LoggerFactory.getLogger(ArticleController.class);

    @Override
    public GraceJSONResult createArticle(NewArticleBO newArticleBO, BindingResult result) {
            if (result.hasErrors()){
                // 判断BindingResult是否保存错误的验证信息,如果有,则直接return
                Map<String, String> errorMap = getErrors(result);
                return GraceJSONResult.errorMap(errorMap);
            }
            // 判断文章封面类型,单图必填,纯文字则设置为空
            if (newArticleBO.getArticleType() == ArticleCoverType.ONE_IMAGE.type){
                if (StringUtils.isBlank(newArticleBO.getArticleCover())){
                    return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_CATEGORY_NOT_EXIST_ERROR);
                }
            } else if (newArticleBO.getArticleType() == ArticleCoverType.WORDS.type) {
                newArticleBO.setArticleCover("");
            }

        // 判断分类id是否存在
        String allCatJson = redis.get(REDIS_ALL_CATEGORY);
        Category temp = null;
        if (StringUtils.isBlank(allCatJson)) {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
        } else {
            List<Category> catList =
                    JsonUtils.jsonToList(allCatJson, Category.class);
            for (Category c : catList) {
                if(c.getId() == newArticleBO.getCategoryId()) {
                    temp = c;
                    break;
                }
            }
            if (temp == null) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_CATEGORY_NOT_EXIST_ERROR);
            }
        }
        return GraceJSONResult.ok();
    }
}
http://writer.imoocnews.com:9090/imooc-news/writer/createArticle.html
dev-model  com/imooc/pojo/bo/NewArticleBO.java
package com.imooc.pojo.bo;

import com.fasterxml.jackson.annotation.JsonFormat;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
 * 用户发文的BO
 */
public class NewArticleBO {

    @NotBlank(message = "文章标题不能为空")
    @Length(max = 30, message = "文章标题长度不能超过30")
    private String title;

    @NotBlank(message = "文章内容不能为空")
    @Length(max = 9999, message = "文章内容长度不能超过10000")
    private String content;

    @NotNull(message = "请选择文章领域")
    private Integer categoryId;

    @NotNull(message = "请选择正确的文章封面类型")
    @Min(value = 1, message = "请选择正确的文章封面类型")
    @Max(value = 2, message = "请选择正确的文章封面类型")
    private Integer articleType;
    private String articleCover;

    @NotNull(message = "文章发布类型不正确")
    @Min(value = 0, message = "文章发布类型不正确")
    @Max(value = 1, message = "文章发布类型不正确")
    private Integer isAppoint;

    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") // 前端日期字符串传到后端后,转换为Date类型
    private Date publishTime;

    @NotBlank(message = "用户未登录")
    private String publishUserId;
}Getter + Setter

发布文章入库Service及联调【也可以定时发布】

http://writer.imoocnews.com:9090/imooc-news/writer/contentMng.html
发布完成后去数据库article中就会存在数据了

generator-datebase  generatorConfig-article.xml [逆向生成]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 通用mapper所在目录 -->
        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="com.imooc.my.mapper.MyMapper"/>
        </plugin>

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/imooc-news-dev"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!-- 对应生成的pojo所在包 -->
        <javaModelGenerator targetPackage="com.imooc.pojo" targetProject="mybatis-generator-database/src/main/java"/>

        <!-- 对应生成的mapper所在目录 -->
        <sqlMapGenerator targetPackage="mapper.article" targetProject="mybatis-generator-database/src/main/resources"/>

        <!-- 配置mapper对应的java映射 -->
        <javaClientGenerator targetPackage="com.imooc.article.mapper" targetProject="mybatis-generator-database/src/main/java" type="XMLMAPPER"/>

        <!-- 数据库表 -->
        <table tableName="comments"></table>

    </context>
</generatorConfiguration>
generator-datebase  com/imooc/mybatis/utils/ArticleGenerator.java
//[运行时候就会自动生成对应文件 目录是上面的generatorConfig-article.xml]
package com.imooc.mybatis.utils;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.File;
import java.util.ArrayList;
import java.util.List;


public class ArticleGenerator {

    public void generator() throws Exception {

        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        //指定 逆向工程配置文件
        File configFile = new File("mybatis-generator-database"
                                            + File.separator
                                            + "generatorConfig-article.xml");
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
                callback, warnings);
        myBatisGenerator.generate(null);

    } 
    
    public static void main(String[] args) throws Exception {
        try {
            ArticleGenerator generatorSqlmap = new ArticleGenerator();
            generatorSqlmap.generator();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
service-article  mapper/ArticleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.ArticleMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Article" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="title" property="title" jdbcType="VARCHAR" />
    <result column="category_id" property="categoryId" jdbcType="INTEGER" />
    <result column="article_type" property="articleType" jdbcType="INTEGER" />
    <result column="article_cover" property="articleCover" jdbcType="VARCHAR" />
    <result column="is_appoint" property="isAppoint" jdbcType="INTEGER" />
    <result column="article_status" property="articleStatus" jdbcType="INTEGER" />
    <result column="publish_user_id" property="publishUserId" jdbcType="VARCHAR" />
    <result column="publish_time" property="publishTime" jdbcType="TIMESTAMP" />
    <result column="read_counts" property="readCounts" jdbcType="INTEGER" />
    <result column="comment_counts" property="commentCounts" jdbcType="INTEGER" />
    <result column="mongo_file_id" property="mongoFileId" jdbcType="VARCHAR" />
    <result column="is_delete" property="isDelete" jdbcType="INTEGER" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
    <result column="update_time" property="updateTime" jdbcType="TIMESTAMP" />
    <result column="content" property="content" jdbcType="LONGVARCHAR" />
  </resultMap>
</mapper>
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;

import java.util.List;

public interface ArticleService {
    /**
     * 发布文章
     */
    public void createArticle(NewArticleBO newArticleBO, Category category);

}
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
package com.imooc.article.service.impl;

import com.imooc.api.service.BaseService;
import com.imooc.article.mapper.ArticleMapper;
import com.imooc.article.service.ArticleService;
import com.imooc.enums.ArticleAppointType;
import com.imooc.enums.ArticleReviewStatus;
import com.imooc.enums.YesOrNo;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.Article;
import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.DateUtil;
import org.apache.commons.lang3.StringUtils;
import org.n3r.idworker.Sid;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

import static com.imooc.api.BaseController.REDIS_ALL_CATEGORY;


@Service
public class ArticleServiceImpl extends BaseService implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper; //红色波浪线就去ArticleMapper上面加@Repository
    @Autowired
    private Sid sid;

    @Transactional
    @Override
    public void createArticle(NewArticleBO newArticleBO, Category category) {
        String articleId = sid.nextShort();

        Article article = new Article();
        BeanUtils.copyProperties(newArticleBO, article);

        article.setId(articleId);
        article.setCategoryId(category.getId());
        article.setArticleStatus(ArticleReviewStatus.REVIEWING.type);
        article.setCommentCounts(0);
        article.setReadCounts(0);

        article.setIsDelete(YesOrNo.NO.type);
        article.setCreateTime(new Date());
        article.setUpdateTime(new Date());

        if (article.getIsAppoint() == ArticleAppointType.TIMING.type) {
            article.setPublishTime(newArticleBO.getPublishTime()); //用户可以在前端选择定时发布
        } else if (article.getIsAppoint() == ArticleAppointType.IMMEDIATELY.type) {
            article.setPublishTime(new Date());
        }

        int res = articleMapper.insert(article);
        if (res != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_CREATE_ERROR);
        }
    }
}
dev-model  com/imooc/pojo/Article.java
package com.imooc.pojo;

import javax.persistence.Column;
import javax.persistence.Id;
import java.util.Date;

public class Article {
    @Id
    private String id;

    /**
     * 文章标题
     */
    private String title;

    /**
     * 文章所属分类id
     */
    @Column(name = "category_id")
    private Integer categoryId;

    /**
     * 文章类型,1:图文(1张封面),2:纯文字
     */
    @Column(name = "article_type")
    private Integer articleType;

    /**
     * 文章封面图,article_type=1 的时候展示
     */
    @Column(name = "article_cover")
    private String articleCover;

    /**
     * 是否是预约定时发布的文章,1:预约(定时)发布,0:即时发布    在预约时间到点的时候,把1改为0,则发布
     */
    @Column(name = "is_appoint")
    private Integer isAppoint;

    /**
     * 文章状态,1:审核中(用户已提交),2:机审结束,等待人工审核,3:审核通过(已发布),4:审核未通过;5:文章撤回(已发布的情况下才能撤回和删除)
     */
    @Column(name = "article_status")
    private Integer articleStatus;

    /**
     * 发布者用户id
     */
    @Column(name = "publish_user_id")
    private String publishUserId;

    /**
     * 文章发布时间(也是预约发布的时间)
     */
    @Column(name = "publish_time")
    private Date publishTime;

    /**
     * 用户累计点击阅读数(喜欢数)(点赞) - 放redis
     */
    @Column(name = "read_counts")
    private Integer readCounts;

    /**
     * 文章评论总数。评论防刷,距离上次评论需要间隔时间控制几秒
     */
    @Column(name = "comment_counts")
    private Integer commentCounts;

    @Column(name = "mongo_file_id")
    private String mongoFileId;

    /**
     * 逻辑删除状态,非物理删除,1:删除,0:未删除
     */
    @Column(name = "is_delete")
    private Integer isDelete;

    /**
     * 文章的创建时间
     */
    @Column(name = "create_time")
    private Date createTime;

    /**
     * 文章的修改时间
     */
    @Column(name = "update_time")
    private Date updateTime;

    /**
     * 文章内容,长度不超过9999,需要在前后端判断
     */
    private String content;

构建定时任务 定时发布文章【定时任务】

在线Cron表达式生成器 (qqe2.com)

service-article  com/imooc/article/task/TaskPublishArticles.java
package com.imooc.article.task;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.time.LocalDateTime;

@Configuration  // 1.标记配置类,使得springboot容器扫描到
@EnableScheduling // 2.开启定时任务
public class TaskPublishArticles {
    @Scheduled(cron = "0/3 * * * * ? ")
    private void publishArticles(){
        System.out.println("执行定时任务:" + LocalDateTime.now());
    }
}

=================================================================
执行定时任务:2024-07-22T14:34:54.009
执行定时任务:2024-07-22T14:34:57.013
执行定时任务:2024-07-22T14:35:00.012
执行定时任务:2024-07-22T14:35:03.002
执行定时任务:2024-07-22T14:35:06.001
执行定时任务:2024-07-22T14:35:09.006
service-article  com/imooc/article/task/TaskPublishArticles.java
package com.imooc.article.task;

import com.imooc.article.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.time.LocalDateTime;

@Configuration  // 1.标记配置类,使得springboot容器扫描到
@EnableScheduling // 2.开启定时任务
public class TaskPublishArticles {
    @Autowired
    private ArticleService articleService;
    // 添加定时任务,注明定时任务的表达式
    // 【若文章数量庞大 需要RabbitMQ去做优化 后面会讲!】
    @Scheduled(cron = "0/3 * * * * ? ")
    private void publishArticles(){
        System.out.println("执行定时任务:" + LocalDateTime.now());
        // 4. 调用文章service,把当前时间应该发布的定时文章,状态改为即时
        articleService.updateAppointToPublish();
    }
}
service-article  com/imooc/article/mapper/ArticleMapperCustom.java
package com.imooc.article.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Article;
import org.springframework.stereotype.Repository;

@Repository
public interface ArticleMapperCustom extends MyMapper<Article> {
    public void updateAppointToPublish();
}
service-article resources/mapper/ArticleMapperCustom.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.ArticleMapperCustom" >
    <update id="updateAppointToPublish">
        update
            article
        set
            is_appoint = 0
        where
            publish_time &lt;= NOW()
        and
            is_appoint = 1
    </update>
</mapper>
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;

import java.util.List;

public interface ArticleService {

    /**
     * 发布文章
     */
    public void createArticle(NewArticleBO newArticleBO, Category category);

    /**
     * 更新定时发布为即使发布
     */
    public void updateAppointToPublish();

}
service-article   com/imooc/article/service/impl/ArticleServiceImpl.java

@Transactional //添加事务[更新操作]
    @Override
    public void updateAppointToPublish() {
        articleMapperCustom.updateAppointToPublish();
    }

文章列表展示 【内容管理】

[mybatis中关于example类详解mybatis的Example

Criteria]的使用 - 万事俱备就差个程序员 - 博客园 (cnblogs.com)

service-api  com/imooc/api/controller/article/ArticleControllerApi.java
@Api(value = "文章业务的controller", tags = {"文章业务的controller"})
@RequestMapping("article")
public interface ArticleControllerApi {

    @PostMapping("createArticle")
    @ApiOperation(value = "用户发文", notes = "用户发文", httpMethod = "POST")
    public GraceJSONResult createArticle(@RequestBody @Valid NewArticleBO newArticleBO, BindingResult result);

    @PostMapping("queryMyList") //对应着前端contentMng.html 340行
    @ApiOperation(value = "查询用户的所有文章列表", notes = "查询用户的所有文章列表", httpMethod = "POST")
    public GraceJSONResult queryMyList(@RequestParam String userId,
                                       @RequestParam String keyword,
                                       @RequestParam Integer status,
                                       @RequestParam Date startDate,
                                       @RequestParam Date endDate,
                                       @RequestParam Integer page,
                                       @RequestParam Integer pageSize);
}
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult queryMyList(String userId, String keyword, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize) {
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_QUERY_PARAMS_ERROR);
        }
        if (page == null){
            page = COMMON_START_PAGE;
        }
        if (pageSize == null){
            pageSize = COMMON_PAGE_SIZE;
        }
        // 查询我的列表,调用service
        PagedGridResult grid =  articleService.queryMyArticleList(userId, keyword, status, startDate, endDate, page, pageSize);
        return GraceJSONResult.ok(grid);
    }
=========================================================================
http://writer.imoocnews.com:9090/imooc-news/writer/contentMng.html
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;
import java.util.List;

public interface ArticleService {

    /**
     * 发布文章
     */
    public void createArticle(NewArticleBO newArticleBO, Category category);

    /**
     * 更新定时发布为即使发布
     */
    public void updateAppointToPublish();

    /**
     * 用户中心-查询我的文章列表
     */
    public PagedGridResult queryMyArticleList(String userId, String keyword, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize);

}
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
@Service
public class ArticleServiceImpl extends BaseService implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper; //红色波浪线就去ArticleMapper上面加@Repository
    @Autowired
    private ArticleMapperCustom articleMapperCustom;
    @Autowired
    private Sid sid;
     //匹配到前端的一种显示方法
    @Override
    public PagedGridResult queryMyArticleList(String userId, String keyword, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize) {
        Example example = new Example(Article.class);
        example.orderBy("createTime").desc();
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("publishUserId", userId);
        if (StringUtils.isNotBlank(keyword)){
            //模糊查询
            criteria.andLike("title", "%"+keyword+"%");
        }
        if (ArticleReviewStatus.isArticleStatusValid(status)){
            // 有效就匹配 无效就查询所有
            criteria.andEqualTo("articleStatus", status);
        }
        // 12是在前端显示审核中
        if (status != null && status == 12){
            criteria.andEqualTo("articleStatus", ArticleReviewStatus.REVIEWING.type)
                    .orEqualTo("articleStatus", ArticleReviewStatus.WAITING_MANUAL.type);
        }
        // 逻辑删除
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        if (startDate != null){ //大于等于
            criteria.andGreaterThanOrEqualTo("publishTime", startDate);
        }
        if (startDate != null){ //小于等于
            criteria.andLessThanOrEqualTo("publishTime",endDate);
        }
        PageHelper.startPage(page, pageSize);
        List<Article> list = articleMapper.selectByExample(example);
        return setterPagedGrid(list,page);
    }
/*
ArticleMapper 可以实现 selectByExample 是因为它继承了 MyMapper 接口,而 MyMapper 提供了一些通用的 CRUD 操作,这些操作包括 selectByExample。

selectByExample 是 MyBatis 提供的一种动态查询方法。它允许你根据条件动态地生成 SQL 查询,而不需要手动编写复杂的 SQL 语句。这在实际开发中非常方便,因为你可以通过构建 Example 对象来动态设置查询条件。

ArticleMapper 继承了 MyMapper<Article>,这意味着它自动获得了 MyMapper 中定义的所有方法,包括 selectByExample。MyMapper 是一个通用的 Mapper 接口,封装了常用的数据库操作方法。

Example 和 Criteria
Example: 用于构建查询条件的对象。在这里,我们创建了一个 Example 对象,用于设置查询的表(Article.class)和排序规则(按 createTime 降序)。

Criteria: 用于添加具体的查询条件。在 Example 对象中创建 Criteria 对象,并使用它来添加各种条件(例如 publishUserId、title、articleStatus、isDelete 等)。

selectByExample
selectByExample 方法使用 Example 对象中的条件动态生成 SQL 查询,并从数据库中获取符合条件的记录。在这个例子中,我们使用了 articleMapper.selectByExample(example) 来根据构建的 Example 对象进行查询。

Example 详细用法
Example 和 Criteria 的使用使得我们可以非常灵活地构建查询条件,而不需要直接拼接 SQL 语句。这不仅提高了代码的可读性,还减少了 SQL 注入的风险。
*/
service-article  com/imooc/article/mapper/ArticleMapper.java
package com.imooc.article.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Article;
import org.springframework.stereotype.Repository;

@Repository
public interface ArticleMapper extends MyMapper<Article> {
}

阿里AI文本检测【内容审核】[机器审核]

dev-common pom.xml 
       <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.0</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.10.2</version>
        </dependency>

        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-green</artifactId>
            <version>3.5.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>
dev-common  com/imooc/utils/extend/AliTextReviewUtils.java
package com.imooc.utils.extend;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.green.model.v20180509.TextScanRequest;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.http.HttpResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;
import java.util.*;

@Component
public class AliTextReviewUtils {

    @Autowired
    private AliyunResource aliyunResource;

    public String reviewTextContent(String content) {
        IClientProfile profile = DefaultProfile.getProfile("cn-shanghai",
                aliyunResource.getAccessKeyID(),
                aliyunResource.getAccessKeySecret());
        IAcsClient client = new DefaultAcsClient(profile);
        TextScanRequest textScanRequest = new TextScanRequest();
        textScanRequest.setAcceptFormat(FormatType.JSON); // 指定api返回格式
        textScanRequest.setHttpContentType(FormatType.JSON);
        textScanRequest.setMethod(com.aliyuncs.http.MethodType.POST); // 指定请求方法
        textScanRequest.setEncoding("UTF-8");
        textScanRequest.setRegionId("cn-shanghai");
        List<Map<String, Object>> tasks = new ArrayList<Map<String, Object>>();
        Map<String, Object> task1 = new LinkedHashMap<String, Object>();
        task1.put("dataId", UUID.randomUUID().toString());
        /**
         * 待检测的文本,长度不超过10000个字符
         */
//        抵制毒品交易
//          尼玛
        task1.put("content", content);
        tasks.add(task1);
        JSONObject data = new JSONObject();

        /**
         * 检测场景,文本垃圾检测传递:antispam
         **/
        data.put("scenes", Arrays.asList("antispam"));
        data.put("tasks", tasks);
        System.out.println(JSON.toJSONString(data, true));

        try {
            textScanRequest.setHttpContent(data.toJSONString().getBytes("UTF-8"), "UTF-8", FormatType.JSON);
            // 请务必设置超时时间
            textScanRequest.setConnectTimeout(3000);
            textScanRequest.setReadTimeout(6000);

            HttpResponse httpResponse = client.doAction(textScanRequest);
            if(httpResponse.isSuccess()){
                JSONObject scrResponse = JSON.parseObject(new String(httpResponse.getHttpContent(), "UTF-8"));
                System.out.println(JSON.toJSONString(scrResponse, true));
                if (200 == scrResponse.getInteger("code")) {
                    JSONArray taskResults = scrResponse.getJSONArray("data");
                    for (Object taskResult : taskResults) {
                        if(200 == ((JSONObject)taskResult).getInteger("code")){
                            JSONArray sceneResults = ((JSONObject)taskResult).getJSONArray("results");
                            JSONObject sceneResult = (JSONObject)sceneResults.get(0);
        //                            for (Object sceneResult : sceneResults) {
                                String scene = sceneResult.getString("scene");
                                String suggestion = sceneResult.getString("suggestion");
                                //根据scene和suggetion做相关处理
                                //suggestion == pass 未命中垃圾  suggestion == block 命中了垃圾,可以通过label字段查看命中的垃圾分类
                                System.out.println("args = [" + scene + "]");
                                System.out.println("args = [" + suggestion + "]");

        //                            suggestion=pass:文本正常,文章状态改为发布通过
        //                            review:需要人工审核,需要在后台管理系统中进行人工审核(很多自媒体平台都会采用机审+人工审的方式)
        //                            block:文本违规,可以直接删除或者做限制处理,审核不通过
        //                            }
                                return suggestion;
                        }else{
                            System.out.println("task process fail:" + ((JSONObject)taskResult).getInteger("code"));
                            return null;
                        }
                    }
                } else {
                    System.out.println("detect not success. code:" + scrResponse.getInteger("code"));
                    return null;
                }
            }else{
                System.out.println("response not success. status:" + httpResponse.getStatus());
                return null;
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
        return null;
    }
}

实现阿里AI自动审核文章【内容审核】

【沿用上面的AliTextReviewUtils】
@Service
public class ArticleServiceImpl extends BaseService implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper; //红色波浪线就去ArticleMapper上面加@Repository
    @Autowired
    private ArticleMapperCustom articleMapperCustom;
    @Autowired
    private AliTextReviewUtils aliTextReviewUtils;
    @Autowired
    private Sid sid;

    @Transactional
    @Override
    public void createArticle(NewArticleBO newArticleBO, Category category) {
        String articleId = sid.nextShort();

        Article article = new Article();
        BeanUtils.copyProperties(newArticleBO, article);

        article.setId(articleId);
        article.setCategoryId(category.getId());
        article.setArticleStatus(ArticleReviewStatus.REVIEWING.type);
        article.setCommentCounts(0);
        article.setReadCounts(0);

        article.setIsDelete(YesOrNo.NO.type);
        article.setCreateTime(new Date());
        article.setUpdateTime(new Date());

        if (article.getIsAppoint() == ArticleAppointType.TIMING.type) {
            article.setPublishTime(newArticleBO.getPublishTime()); //用户可以在前端选择定时发布
        } else if (article.getIsAppoint() == ArticleAppointType.IMMEDIATELY.type) {
            article.setPublishTime(new Date());
        }

        int res = articleMapper.insert(article);
        if (res != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_CREATE_ERROR);
        }

        /**
         * FIXME: 我们只检测正常的词汇,非正常词汇大家课后去检测
         */
        // 通过阿里智能AI实现对文章文本的自动检测(自动审核)
//        String reviewTextResult = aliTextReviewUtils.reviewTextContent(newArticleBO.getContent());
        String reviewTextResult = ArticleReviewLevel.REVIEW.type;

        if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.PASS.type)) {
            // 修改当前的文章,状态标记为审核通过
            this.updateArticleStatus(articleId, ArticleReviewStatus.SUCCESS.type);
        } else if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.REVIEW.type)) {
            // 修改当前的文章,状态标记为需要人工审核
            this.updateArticleStatus(articleId, ArticleReviewStatus.WAITING_MANUAL.type);
        } else if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.BLOCK.type)) {
            // 修改当前的文章,状态标记为审核未通过
            this.updateArticleStatus(articleId, ArticleReviewStatus.FAILED.type);
        }
    }
......
......
    @Transactional
    @Override
    public void updateArticleStatus(String articleId, Integer pendingStatus) {
        Example example = new Example(Article.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("id",articleId);

        Article pendingArticle = new Article();
        pendingArticle.setArticleStatus(pendingStatus);
        int res = articleMapper.updateByExampleSelective(pendingArticle, example);
        if (res != 1){
            GraceException.display(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
    }
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;
import java.util.List;

public interface ArticleService {

    /**
     * 发布文章
     */
    public void createArticle(NewArticleBO newArticleBO, Category category);

    /**
     * 更新定时发布为即使发布
     */
    public void updateAppointToPublish();

    /**
     * 用户中心-查询我的文章列表
     */
    public PagedGridResult queryMyArticleList(String userId, String keyword, Integer status, Date startDate, Date endDate, Integer page, Integer pageSize);

    /**
     * 更改文章的状态
     * @param articleId
     * @param pendingStatus
     */
    public void updateArticleStatus(String articleId, Integer pendingStatus);
}

admin文章管理列表【内容审核】【作业】

管理员查询用户的所有文章列表
service-api  com/imooc/api/controller/article/ArticleControllerApi.java
@PostMapping("queryAllList")
    @ApiOperation(value = "管理员查询用户的所有文章列表", notes = "管理员查询用户的所有文章列表", httpMethod = "POST")
    public GraceJSONResult queryAllList(@RequestParam Integer status,
                                        @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                                        @RequestParam Integer page,
                                        @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                                        @RequestParam Integer pageSize);
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult queryAllList(Integer status, Integer page, Integer pageSize) {
        if (page == null){
            page = COMMON_START_PAGE;
        }
        if (pageSize == null){
            pageSize = COMMON_PAGE_SIZE;
        }
        PagedGridResult gridResult = articleService.queryAllArticleListAdmin(status,page,pageSize);

        return GraceJSONResult.ok(gridResult);
    }
service-article  com/imooc/article/service/ArticleService.java
    /**
     * 管理员查询文章列表
     */
    public PagedGridResult queryAllArticleListAdmin(Integer status, Integer page, Integer pageSize);
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
@Override
    public PagedGridResult queryAllArticleListAdmin(Integer status, Integer page, Integer pageSize) {
        Example articleExample = new Example(Article.class);
        articleExample.orderBy("createTime").desc();
        Example.Criteria criteria = articleExample.createCriteria();
        //这里是检测文章状态 与前端做匹配
        if (ArticleReviewStatus.isArticleStatusValid(status)) {
            criteria.andEqualTo("articleStatus", status);
        }

        // 审核中是机审和人审核的两个状态,所以需要单独判断
        if (status != null && status == 12) {
            criteria.andEqualTo("articleStatus", ArticleReviewStatus.REVIEWING.type)
                    .orEqualTo("articleStatus", ArticleReviewStatus.WAITING_MANUAL.type);
        }
        //isDelete必须是0
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        /**
         * page: 第几页
         * pageSize: 每页显示条数
         */
        PageHelper.startPage(page, pageSize);
        List<Article> list = articleMapper.selectByExample(articleExample);
        return setterPagedGrid(list, page);
    }

人工审核 【内容审核】

内容审核 | 运营管理平台 (imoocnews.com) 【[待审核]手动审核通过】

service-api  com/imooc/api/controller/article/ArticleControllerApi.java
    @PostMapping("doReview")
    @ApiOperation(value = "管理员对文章进行审核通过或者失败", notes = "管理员对文章进行审核通过或者失败", httpMethod = "POST")
    public GraceJSONResult doReview(@RequestParam String articleId,
                                    @RequestParam Integer passOrNot);
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);
        return GraceJSONResult.ok();
    }

撤回_删除文章作业 【内容管理】

媒体号作家中心 | 内容管理 (imoocnews.com)
[这个是用户撤回和删除噢 而不是管理员的撤回与删除]
用户:媒体号作家中心 | 内容管理 (imoocnews.com)
管理员:内容审核 | 运营管理平台 (imoocnews.com)

service-api  com/imooc/api/controller/article/ArticleControllerApi.java
    @PostMapping("/delete")
    @ApiOperation(value = "用户删除文章", notes = "用户删除文章", httpMethod = "POST")
    public GraceJSONResult delete(@RequestParam String userId,
                                  @RequestParam String articleId);

    @PostMapping("/withdraw")
    @ApiOperation(value = "用户撤回文章", notes = "用户撤回文章", httpMethod = "POST")
    public GraceJSONResult withdraw(@RequestParam String userId,
                                    @RequestParam String articleId);
}
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult delete(String userId, String articleId) {
        articleService.deleteArticle(userId,articleId);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult withdraw(String userId, String articleId) {
        articleService.withdrawArticle(userId, articleId);
        return GraceJSONResult.ok();
    }
service-article  com/imooc/article/service/ArticleService.java
package com.imooc.article.service;

import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.PagedGridResult;

import java.util.Date;
import java.util.List;

public interface ArticleService {

   /**
     * 删除文章
     */
    public void deleteArticle(String userId, String articleId);

    /**
     * 撤回文章
     */
    public void withdrawArticle(String userId, String articleId);
}
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
 @Transactional
    @Override
    public void deleteArticle(String userId, String articleId) {
        Example articleExample = makeExampleCriteria(userId, articleId);

        Article pending = new Article();
        pending.setIsDelete(YesOrNo.YES.type);

        int result = articleMapper.updateByExampleSelective(pending, articleExample);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_DELETE_ERROR);
        }
    }

    @Transactional
    @Override
    public void withdrawArticle(String userId, String articleId) {
        Example articleExample = makeExampleCriteria(userId, articleId);

        Article pending = new Article();
        pending.setArticleStatus(ArticleReviewStatus.WITHDRAW.type);

        int result = articleMapper.updateByExampleSelective(pending, articleExample);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_WITHDRAW_ERROR);
        }
//        deleteHTML(articleId);
    }

    private Example makeExampleCriteria(String userId, String articleId) {
        Example articleExample = new Example(Article.class);
        Example.Criteria criteria = articleExample.createCriteria();
        criteria.andEqualTo("publishUserId", userId);
        criteria.andEqualTo("id", articleId);
        return articleExample;
    }

首页_作者页面介绍【章节描述】

  • 开发首页与作家个人展示页
  • 文章列表、友情链接查询
  • 粉丝关注与取关
  • 我的粉丝与粉丝画像

根据MongoDB字段查询友情链接

service-api  com/imooc/api/controller/admin/FriendLinkControllerApi.java
    @ApiOperation(value = "门户端查询友情链接列表", notes = "门户端查询友情链接列表", httpMethod = "GET")
    @GetMapping("portal/list")
    public GraceJSONResult queryPortalAllFriendLinkList();
service-admin  com/imooc/admin/controller/FriendLinkController.java
    @Override
    public GraceJSONResult queryPortalAllFriendLinkList() {
        List<FriendLinkMO> list = friendLinkService.queryPortalAllFriendLinkList();
        return GraceJSONResult.ok(list);
    }
service-admin  com/imooc/admin/service/FriendLinkService.java
    /**
     * 首页查询友情链接
     */
    public List<FriendLinkMO> queryPortalAllFriendLinkList();
service-admin com/imooc/admin/service/impl/FriendLinkServiceImpl.java 
@Override
    public List<FriendLinkMO> queryPortalAllFriendLinkList() {
        return friendLinkRepository.getAllByIsDelete(YesOrNo.NO.type);
    }
service-admin  com/imooc/admin/repository/FriendLinkRepository.java
@Repository
public interface FriendLinkRepository extends MongoRepository<FriendLinkMO, String> { //持久层
    // 内置提供了很多方法 find.. delete...
    public List<FriendLinkMO> getAllByIsDelete(Integer isDelete); //后面可以加ANDID
}

搜索并展示文章列表【首页】

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@Api(value = "门户端文章业务的controller", tags = {"门户端文章业务的controller"})
@RequestMapping("portal/article")
public interface ArticlePortalControllerApi {
    @ApiOperation(value = "首页查询文章列表", notes = "首页查询文章列表", httpMethod = "GET")
    @GetMapping("list")
    public GraceJSONResult list(@RequestParam String keyword,
                                @RequestParam Integer category,
                                @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                                @RequestParam Integer page,
                                @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                                @RequestParam Integer pageSize);
}
service-article  com/imooc/article/controller/ArticlePortalController.java
@RestController
public class ArticlePortalController extends BaseController implements ArticlePortalControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticlePortalController.class);

    @Autowired
    private ArticlePortalService articlePortalService;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
        return GraceJSONResult.ok(gridResult);
    }
}
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java
@Service
public class ArticlePortalServiceImpl extends BaseService implements ArticlePortalService {
    @Autowired
    private ArticleMapper articleMapper; //红色波浪线就去ArticleMapper上面加@Repository


    @Override
    public PagedGridResult queryIndexArticleList(String keyword,
                                                 Integer category,
                                                 Integer page,
                                                 Integer pageSize) {

        Example articleExample = new Example(Article.class);
        articleExample.orderBy("publishTime").desc();//使用时间进行排序
        Example.Criteria criteria = articleExample.createCriteria();

        /**
         * 查询首页文章的自带隐性查询条件:
         * isAppoint=即使发布,表示文章已经直接发布的,或者定时任务到点发布的
         * isDelete=未删除,表示文章只能够显示未删除
         * articleStatus=审核通过,表示只有文章经过机审/人工审核之后才能展示
         */
        criteria.andEqualTo("isAppoint", YesOrNo.NO.type);
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        criteria.andEqualTo("articleStatus", ArticleReviewStatus.SUCCESS.type);

        if (StringUtils.isNotBlank(keyword)) {
            criteria.andLike("title", "%" + keyword + "%");
        }
        if (category != null) {
            criteria.andEqualTo("categoryId", category);
        }

        PageHelper.startPage(page, pageSize);
        List<Article> list = articleMapper.selectByExample(articleExample);
        System.out.println(keyword);
        System.out.println(category);
        return setterPagedGrid(list, page);
    }
}
service-article  com/imooc/article/service/ArticlePortalService.java
public interface ArticlePortalService {

    /**
     * 首页查询文章列表
     */
    public PagedGridResult queryIndexArticleList(String keyword,
                                                 Integer category,
                                                 Integer page,
                                                 }
index.html
<!-- 中间容器 -->
        <div class="container">
            <!-- 文章列表 -->
            <div id="articleList" class="article-list">
                <ul>
                    <li class="single-article-wrapper" v-for="(article, index) in articleList" :key="index">
                        <img :src="article.articleCover" class="article-cover" v-show="article.articleType == 1">

                        <div class="single-article">
                            <div class="article-title">
                                <!-- TODO: 后期改为静态页面跳转 -->
                                <a :href="'detail.html?articleId='+article.id" target="_blank" class="link-article-title">{{article.title}}</a>
                            </div>
                            <div class="publisher">
                                <div class="category-tag" :style="{color: getCatTagColor(article.categoryId), borderColor: getCatTagColor(article.categoryId) }">{{getCatName(article.categoryId)}}</div>
                                <!-- TODO: 这里需要显示用户的昵称以及用户头像 -->
                                    <img src="img/face1.png" class="publisher-face" v-if="article.publisherVO == null || article.publisherVO == undefined">
                                    <div class="publisher-name" v-if="article.publisherVO == null || article.publisherVO == undefined">&nbsp;&nbsp;{{article.publishUserId}}&nbsp;⋅</div>
                                
                                
                                    <img :src="article.publisherVO.face" class="publisher-face" v-if="article.publisherVO != null && article.publisherVO != undefined">
                                <!--
                                    <a :href="'writer.html?writerId='+article.publisherVO.id" target="_blank">
                                    <div class="publisher-name" v-if="article.publisherVO != null && article.publisherVO != undefined">&nbsp;&nbsp;{{article.publisherVO.nickname}}&nbsp;⋅</div>
                                </a>
                                    -->

                                <div class="article-name">&nbsp;{{article.readCounts}}阅读&nbsp;⋅</div>
                                
                                <!-- <div class="publish-time">&nbsp;{{formatData(article.publishTime)}}</div> -->
                                <div class="publish-time">&nbsp;{{getDateBeforeNow(article.publishTime)}}</div>
                            </div>
                        </div>
                    </li>
                </ul>
            </div>

文章列表展示发布者需求【首页】

service-article  com/imooc/article/controller/ArticlePortalController.java
[其他不变加上点代码]
package com.imooc.article.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.article.ArticleControllerApi;
import com.imooc.api.controller.article.ArticlePortalControllerApi;
import com.imooc.article.service.ArticlePortalService;
import com.imooc.article.service.ArticleService;
import com.imooc.enums.ArticleCoverType;
import com.imooc.enums.ArticleReviewStatus;
import com.imooc.enums.YesOrNo;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.Article;
import com.imooc.pojo.Category;
import com.imooc.pojo.bo.NewArticleBO;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.*;

@RestController
public class ArticlePortalController extends BaseController implements ArticlePortalControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticlePortalController.class);

    @Autowired
    private ArticlePortalService articlePortalService;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
   //START
        List<Article> list = (List<Article>) gridResult.getRows();
        // 1. 构建发布者id列表
        Set<String> idset = new HashSet<>();
        for (Article a : list){
//            System.out.println(a.getPublishUserId());
            idset.add(a.getPublishUserId());
        }
        System.out.println(idset.toString());
        // 2. 发起远程调用(restTemplate),请求用户服务获得用户(idSet 发布者)列表

        // 3. 拼接两个list,重组文章列表
   //END
        return GraceJSONResult.ok(gridResult);
    }
}

发起restTemplate请求查询用户服务获得发布者列表【首页】二级用户

service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
   //START 用户量大就双表关联查询      单表双查询 → 【首页不会显示发布者的用户id 和 头像】
        List<Article> list = (List<Article>) gridResult.getRows();
        // 1. 构建发布者id列表
        Set<String> idset = new HashSet<>();
        for (Article a : list){
//            System.out.println(a.getPublishUserId());
            idset.add(a.getPublishUserId());
        }
        System.out.println(idset.toString());
        // 2. 发起远程调用(restTemplate),请求用户服务获得用户(idSet 发布者)列表
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idset);
        ResponseEntity<GraceJSONResult> responseEntity =
        restTemplate.getForEntity(userServerUrlExecute,GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200){
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        for (AppUserVO u : publisherList){
            System.out.println(u.toString());
        }
        // 3. 拼接两个list,重组文章列表
   //END
        return GraceJSONResult.ok(gridResult);
    }
===================成功输出二级用户基本信息===============================
AppUserVO{id='240629F21AK1BHX4', nickname='15027597319', face='https://iimooc-news-dev.oss-cn-shanghai.aliyuncs.com/images/abc/240629F21AK1BHX4/240712FM0G1WMZHH.png', activeStatus=1}
AppUserVO{id='200628AFYM7AGWPH', nickname='我是慕课网', face='https://imooc-news-dev.oss-cn-shanghai.aliyuncs.com/images/abc/200628AFYM7AGWPH/2007088XH2WT7GXP.png', activeStatus=1}
dev-model  com/imooc/pojo/vo/AppUserVO.java
public class AppUserVO {
    private String id;
    private String nickname;
    private String face;
    private Integer activeStatus;
}Getter + Setter + ToString
service-user  com/imooc/user/controller/UserController.java
@RestController
public class UserController extends BaseController implements UserControllerApi {
@Override
    public GraceJSONResult queryByIds(String userIds) {
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }

    private AppUserVO getBasicUserInfo(String userId){
        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        AppUserVO userVO = new AppUserVO();
        BeanUtils.copyProperties(user, userVO); //拷贝信息
        return userVO;
    }
}
service-api  com/imooc/api/controller/user/UserControllerApi.java
package com.imooc.api.controller.user;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
public interface UserControllerApi {

    @ApiOperation(value = "获得用户基本信息",notes = "获得用户基本信息",httpMethod = "POST")
    @PostMapping("/getUserInfo")
    public GraceJSONResult getUserInfo(@RequestParam String userId);
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
                                          BindingResult result);

    @ApiOperation(value = "根据用户的ids查询用户列表",notes = "根据用户的ids查询用户列表",httpMethod = "GET")
    @GetMapping("/queryByids")
    public GraceJSONResult queryByIds(@RequestParam String userIds);
}

重组文章列表并且渲染【首页】

index.html
<!-- 中间容器 -->
        <div class="container">
            <!-- 文章列表 -->
            <div id="articleList" class="article-list">
                <ul>
                    <li class="single-article-wrapper" v-for="(article, index) in articleList" :key="index">
                        <img :src="article.articleCover" class="article-cover" v-show="article.articleType == 1">

                        <div class="single-article">
                            <div class="article-title">
                                <!-- TODO: 后期改为静态页面跳转 -->
                                <a :href="'detail.html?articleId='+article.id" target="_blank" class="link-article-title">{{article.title}}</a>
                            </div>
                            <div class="publisher">
                                <div class="category-tag" :style="{color: getCatTagColor(article.categoryId), borderColor: getCatTagColor(article.categoryId) }">{{getCatName(article.categoryId)}}</div>
          <!-- ★★★★★ TODO: 这里需要显示用户的昵称以及用户头像 ★★★★★ -->
                                    <img src="img/face1.png" class="publisher-face" v-if="article.publisherVO == null || article.publisherVO == undefined">
                                    <div class="publisher-name" v-if="article.publisherVO == null || article.publisherVO == undefined">&nbsp;&nbsp;{{article.publishUserId}}&nbsp;⋅</div>
                                    <img :src="article.publisherVO.face" class="publisher-face" v-if="article.publisherVO != null && article.publisherVO != undefined">
                                
                                    <a :href="'writer.html?writerId='+article.publisherVO.id" target="_blank">
                                    <div class="publisher-name" v-if="article.publisherVO != null && article.publisherVO != undefined">&nbsp;&nbsp;{{article.publisherVO.nickname}}&nbsp;⋅</div>
                                </a>            
                                <div class="article-name">&nbsp;{{article.readCounts}}阅读&nbsp;⋅</div>
                                <!-- <div class="publish-time">&nbsp;{{formatData(article.publishTime)}}</div> -->
                                <div class="publish-time">&nbsp;{{getDateBeforeNow(article.publishTime)}}</div>
                            </div>
                        </div>
                    </li>
                </ul>
            </div>
dev-model  com/imooc/pojo/vo/IndexArticleVO.java
public class IndexArticleVO {

    private String id;
    private String title;
    private Integer categoryId;
    private Integer articleType;
    private String articleCover;
    private Integer isAppoint;
    private Integer articleStatus;
    private String publishUserId;
    private Date publishTime;
    private Integer readCounts;
    private Integer commentCounts;
    private String mongoFileId;
    private Integer isDelete;
    private Date createTime;
    private Date updateTime;
    private String content;
}Getter+Setter
service-article  com/imooc/article/controller/ArticlePortalController.java
package com.imooc.article.controller;
......
import java.util.*;

@RestController
public class ArticlePortalController extends BaseController implements ArticlePortalControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticlePortalController.class);

    @Autowired
    private ArticlePortalService articlePortalService;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
   //START 用户量大就双表关联查询      单表双查询 → 【首页不会显示发布者的用户id 和 头像】
        List<Article> list = (List<Article>) gridResult.getRows();
        // 1. 构建发布者id列表
        Set<String> idset = new HashSet<>();
        for (Article a : list){
//            System.out.println(a.getPublishUserId());
            idset.add(a.getPublishUserId());
        }
        System.out.println(idset.toString());
        // 2. 发起远程调用(restTemplate),请求用户服务获得用户(idSet 发布者)列表
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idset);
        ResponseEntity<GraceJSONResult> responseEntity =
        restTemplate.getForEntity(userServerUrlExecute,GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200){
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
//        for (AppUserVO u : publisherList){
//            System.out.println(u.toString());
//        }
        // 3. 拼接两个list,重组文章列表
        List<IndexArticleVO> indexArticleList = new ArrayList<>();
        for (Article a : list){
            IndexArticleVO indexArticleVO = new IndexArticleVO();
            BeanUtils.copyProperties(a, indexArticleVO);

            // 3.1 从publisherList中获得发布者的基本信息
            AppUserVO publisher = getUserIfPublisher(a.getPublishUserId(), publisherList);
            indexArticleVO.setPublisherVO(publisher);
            indexArticleList.add(indexArticleVO);
        }
        gridResult.setRows(indexArticleList);
   //END
        return GraceJSONResult.ok(gridResult);
    }
    // 用于获得publish
    private AppUserVO getUserIfPublisher(String publisherId, List<AppUserVO> publisherList){
        for (AppUserVO user : publisherList){
            if (user.getId().equalsIgnoreCase(publisherId)){
                return user;
            }
        }
        return null;
    }
}

查询热闻【首页】阅读数从最新新闻进行排名

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@Api(value = "门户端文章业务的controller", tags = {"门户端文章业务的controller"})
@RequestMapping("portal/article")
public interface ArticlePortalControllerApi {
 @GetMapping("hotList")
    @ApiOperation(value = "首页查询新闻列表", notes = "首页查询新闻列表", httpMethod = "GET")
    public GraceJSONResult hotList();
}
service-article  com/imooc/article/service/ArticlePortalService.java
public interface ArticlePortalService {

    /**
     * 首页查询文章列表
     */
    public PagedGridResult queryIndexArticleList(String keyword,
                                                 Integer category,
                                                 Integer page,
                                                 Integer pageSize);
    /**
     * 首页查询热闻列表
     */
    public List<Article> queryHotList();
}
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java
@Override
    public List<Article> queryHotList() {
        Example articleExample = new Example(Article.class);
        Example.Criteria criteria = setDefualArticleExample(articleExample);

        PageHelper.startPage(1, 5);
        List<Article> list  = articleMapper.selectByExample(articleExample);
        return list;
    }

    private Example.Criteria setDefualArticleExample(Example articleExample) {
        articleExample.orderBy("publishTime").desc();
        Example.Criteria criteria = articleExample.createCriteria();

        /**
         * 查询首页文章的自带隐性查询条件:
         * isAppoint=即使发布,表示文章已经直接发布的,或者定时任务到点发布的
         * isDelete=未删除,表示文章只能够显示未删除
         * articleStatus=审核通过,表示只有文章经过机审/人工审核之后才能展示
         */
        criteria.andEqualTo("isAppoint", YesOrNo.NO.type);
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        criteria.andEqualTo("articleStatus", ArticleReviewStatus.SUCCESS.type);

        return criteria;
    }
service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public GraceJSONResult hotList() {
        return GraceJSONResult.ok(articlePortalService.queryHotList());
    }

基本信息展示_历史文章列表【作者主页】

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
 package com.imooc.api.controller.article;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewArticleBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.Date;

@Api(value = "门户端文章业务的controller", tags = {"门户端文章业务的controller"})
@RequestMapping("portal/article")
public interface ArticlePortalControllerApi {

    @GetMapping("list")
    @ApiOperation(value = "首页查询文章列表", notes = "首页查询文章列表", httpMethod = "GET")
    public GraceJSONResult list(@RequestParam String keyword,
                                @RequestParam Integer category,
                                @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                                @RequestParam Integer page,
                                @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                                @RequestParam Integer pageSize);

    @GetMapping("hotList")
    @ApiOperation(value = "首页查询新闻列表", notes = "首页查询新闻列表", httpMethod = "GET")
    public GraceJSONResult hotList();

    /**
     * 查询作家发布的所有文章列表
     */
    @GetMapping("queryArticleListOfWriter")
    @ApiOperation(value = "查询作家发布的所有文章列表", notes = "查询作家发布的所有文章列表", httpMethod = "GET")
    public GraceJSONResult queryArticleListOfWriter(@RequestParam String writerId,
                                                    @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                                                        @RequestParam Integer page,
                                                    @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                                                        @RequestParam Integer pageSize);

    @GetMapping("queryGoodArticleListOfWriter")
    @ApiOperation(value = "作家页面查询近期佳文", notes = "作家页面查询近期佳文", httpMethod = "GET")
    public GraceJSONResult queryGoodArticleListOfWriter(@RequestParam String writerId);
}
service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public GraceJSONResult queryArticleListOfWriter(String writerId, Integer page, Integer pageSize) {

        System.out.println("writerId=" + writerId);

        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult = articlePortalService.queryArticleListOfWriter(writerId, page, pageSize);
        gridResult = rebuildArticleGrid(gridResult);
        return GraceJSONResult.ok(gridResult);
    }

    @Override
    public GraceJSONResult queryGoodArticleListOfWriter(String writerId) {
        PagedGridResult gridResult = articlePortalService.queryGoodArticleListOfWriter(writerId);
        return GraceJSONResult.ok(gridResult);
    }
}
/* 完全版ArticlePortalController
package com.imooc.article.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.article.ArticlePortalControllerApi;
import com.imooc.article.service.ArticlePortalService;
import com.imooc.article.service.ArticleService;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.Article;
import com.imooc.pojo.vo.AppUserVO;
import com.imooc.pojo.vo.IndexArticleVO;
import com.imooc.utils.IPUtil;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@RestController
public class ArticlePortalController extends BaseController implements ArticlePortalControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticlePortalController.class);

    @Autowired
    private ArticlePortalService articlePortalService;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public GraceJSONResult list(String keyword,
                                Integer category,
                                Integer page,
                                Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult
                = articlePortalService.queryIndexArticleList(keyword,
                category,
                page,
                pageSize);
        gridResult = rebuildArticleGrid(gridResult);
        return GraceJSONResult.ok(gridResult);
    }

    private PagedGridResult rebuildArticleGrid(PagedGridResult gridResult) {
        // START

        List<Article> list = (List<Article>)gridResult.getRows();

        // 1. 构建发布者id列表
        Set<String> idSet = new HashSet<>();
        List<String> idList = new ArrayList<>();
        for (Article a : list) {
//            System.out.println(a.getPublishUserId());
            // 1.1 构建发布者的set
            idSet.add(a.getPublishUserId());
            // 1.2 构建文章id的list
            idList.add(REDIS_ARTICLE_READ_COUNTS + ":" + a.getId());
        }
        System.out.println(idSet.toString());
        // 发起redis的mget批量查询api,获得对应的值
        List<String> readCountsRedisList = redis.mget(idList);
        List<AppUserVO> publisherList = getPublisherList(idSet);

        // 3. 拼接两个list,重组文章列表
        List<IndexArticleVO> indexArticleList = new ArrayList<>();
        for (int i = 0 ; i < list.size() ; i ++) {
            IndexArticleVO indexArticleVO = new IndexArticleVO();
            Article a = list.get(i);
            BeanUtils.copyProperties(a, indexArticleVO);

            // 3.1 从publisherList中获得发布者的基本信息
            AppUserVO publisher  = getUserIfPublisher(a.getPublishUserId(), publisherList);
            indexArticleVO.setPublisherVO(publisher);

            // 3.2 重新组装设置文章列表中的阅读量
            String redisCountsStr = readCountsRedisList.get(i);
            int readCounts = 0;
            if (StringUtils.isNotBlank(redisCountsStr)) {
                readCounts = Integer.valueOf(redisCountsStr);
            }
            indexArticleVO.setReadCounts(readCounts);

            indexArticleList.add(indexArticleVO);
        }


        gridResult.setRows(indexArticleList);
// END
        return gridResult;
    }

    private AppUserVO getUserIfPublisher(String publisherId,
                                         List<AppUserVO> publisherList) {
        for (AppUserVO user : publisherList) {
            if (user.getId().equalsIgnoreCase(publisherId)) {
                return user;
            }
        }
        return null;
    }

    // 发起远程调用,获得用户的基本信息
    private List<AppUserVO> getPublisherList(Set idSet) {
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return publisherList;
    }

    @Override
    public GraceJSONResult hotList() {
        return GraceJSONResult.ok(articlePortalService.queryHotList());
    }

    @Override
    public GraceJSONResult queryArticleListOfWriter(String writerId, Integer page, Integer pageSize) {

        System.out.println("writerId=" + writerId);

        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult = articlePortalService.queryArticleListOfWriter(writerId, page, pageSize);
        gridResult = rebuildArticleGrid(gridResult);
        return GraceJSONResult.ok(gridResult);
    }

    @Override
    public GraceJSONResult queryGoodArticleListOfWriter(String writerId) {
        PagedGridResult gridResult = articlePortalService.queryGoodArticleListOfWriter(writerId);
        return GraceJSONResult.ok(gridResult);
    }
}

*/
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java
public interface ArticlePortalService {

    /**
     * 首页查询文章列表
     */
    public PagedGridResult queryIndexArticleList(String keyword,
                                                 Integer category,
                                                 Integer page,
                                                 Integer pageSize);
    /**
     * 首页查询热闻列表
     */
    public List<Article> queryHotList();

    /**
     * 查询作家发布的所有文章列表
     */
    public PagedGridResult queryArticleListOfWriter(String writerId,
                                                    Integer page,
                                                    Integer pageSize);

    /**
     * 作家页面查询近期佳文
     */
    public PagedGridResult queryGoodArticleListOfWriter(String writerId);
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java

@Override
    public PagedGridResult queryArticleListOfWriter(String writerId, Integer page, Integer pageSize) {
        Example articleExample = new Example(Article.class);

        Example.Criteria criteria = setDefualArticleExample(articleExample);
        criteria.andEqualTo("publishUserId", writerId);

        /**
         * page: 第几页
         * pageSize: 每页显示条数
         */
        PageHelper.startPage(page, pageSize);
        List<Article> list = articleMapper.selectByExample(articleExample);
        return setterPagedGrid(list, page);
    }

    @Override
    public PagedGridResult queryGoodArticleListOfWriter(String writerId) {
        Example articleExample = new Example(Article.class);
        articleExample.orderBy("publishTime").desc();

        Example.Criteria criteria = setDefualArticleExample(articleExample);
        criteria.andEqualTo("publishUserId", writerId);

        /**
         * page: 第几页
         * pageSize: 每页显示条数
         */
        PageHelper.startPage(1, 5);
        List<Article> list = articleMapper.selectByExample(articleExample);
        return setterPagedGrid(list, 1);
    }

    private Example.Criteria setDefualArticleExample(Example articleExample) {
        articleExample.orderBy("publishTime").desc();
        Example.Criteria criteria = articleExample.createCriteria();

        /**
         * 查询首页文章的自带隐性查询条件:
         * isAppoint=即使发布,表示文章已经直接发布的,或者定时任务到点发布的
         * isDelete=未删除,表示文章只能够显示未删除
         * articleStatus=审核通过,表示只有文章经过机审/人工审核之后才能展示
         */
        criteria.andEqualTo("isAppoint", YesOrNo.NO.type);
        criteria.andEqualTo("isDelete", YesOrNo.NO.type);
        criteria.andEqualTo("articleStatus", ArticleReviewStatus.SUCCESS.type);

        return criteria;
    }

关注与取关_redis单线程计数统计 【粉丝关注】

阅读数可以用数据库COUNT* 但是压力会很大 若很多人一起刷新会音响很大
用redis 数量累加累减 单线程安全
减少数据库压力

【注意 redis我安装到了本地计算机里面 D:\Redis-x64-3.0.504】
打开redis-cli.exe
127.0.0.1:6379> keys *
1) "redis_all_category"
2) "redis_admin_token:1001"
3) "redis_user_info:1001"
4) "redis_user_info:200628AFYM7AGWPH"
5) "redis_user_token:240629F21AK1BHX4"
6) "redis_user_info:240629F21AK1BHX4"
7) "redis_user_token:200628AFYM7AGWPH"
127.0.0.1:6379> INCR 1001:fans #【增加】
(integer) 1
127.0.0.1:6379> INCR 1001:fans
(integer) 2
127.0.0.1:6379> INCR 1001:fans
(integer) 3
127.0.0.1:6379> get 1001:fans #【获取】
"3"
127.0.0.1:6379> DECR 1001:fans #【减少】
(integer) 2
127.0.0.1:6379> DECR 1001:fans
(integer) 1
127.0.0.1:6379> incr 1001:follows #【关注的粉丝】
(integer) 1
127.0.0.1:6379> get 1001:follows
"1"

查询用户关注状态【粉丝关注】

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

@Api(value = "粉丝管理",tags = {"粉丝管理功能的controller"})
@RequestMapping("fans")
public interface MyFansControllerApi {
    @ApiOperation(value = "查询当前用户是否关注作家",notes = "查询当前用户是否关注作家",httpMethod = "POST")
    @PostMapping("/isMeFollowThisWriter")
    public GraceJSONResult isMeFollowThisWriter(@RequestParam String writerId, @RequestParam String fanId);
}
service-user  com/imooc/user/controller/MyFansController.java
package com.imooc.user.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.api.controller.user.MyFansControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.user.service.MyFansService;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
    final static Logger logger = LoggerFactory.getLogger(MyFansController.class);

    @Autowired
    private MyFansService myFansService;

    @Override
    public GraceJSONResult isMeFollowThisWriter(String writerId, String fanId) {
        boolean res = myFansService.isMeFollowThisWriter(writerId,fanId);
        return GraceJSONResult.ok(res);
    }
}
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

import com.imooc.utils.PagedGridResult;

import java.util.Date;

public interface MyFansService {
    /**
     * 查询当前用户是否关注作家
     */
    public boolean isMeFollowThisWriter(String writerId, String fanId);

}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
package com.imooc.user.service.impl;

import com.github.pagehelper.PageHelper;
import com.imooc.api.service.BaseService;
import com.imooc.enums.UserStatus;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.Fans;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.mapper.FansMapper;
import com.imooc.user.service.AppUserMngService;
import com.imooc.user.service.MyFansService;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
    @Autowired
    public FansMapper fansMapper;


    @Override
    public boolean isMeFollowThisWriter(String writerId, String fanId) {
        Fans fan = new Fans();
        fan.setFanId(fanId);
        fan.setWriterId(writerId);
        int count = fansMapper.selectCount(fan); //前期先放在数据库里
        return count > 0 ? true : false;
    }
}
service-user  com/imooc/user/mapper/FansMapper.java
package com.imooc.user.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Fans;
import org.springframework.stereotype.Repository;

@Repository
public interface FansMapper extends MyMapper<Fans> {
}

用户关注_粉丝累加 && 粉丝累减

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
package com.imooc.api.controller.user;


import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;

@Api(value = "粉丝管理",tags = {"粉丝管理功能的controller"})
@RequestMapping("fans")
public interface MyFansControllerApi {
    @ApiOperation(value = "用户关注作家,成为粉丝",notes = "用户关注作家,成为粉丝",httpMethod = "POST")
    @PostMapping("/follow")
    public GraceJSONResult follow(@RequestParam String writerId, @RequestParam String fanId);
    
    @ApiOperation(value = "取消关注,作家损失粉丝",notes = "取消关注,作家损失粉丝",httpMethod = "POST")
    @PostMapping("/unfollow")
    public GraceJSONResult unfollow(@RequestParam String writerId, @RequestParam String fanId);
}
service-user  com/imooc/user/controller/MyFansController.java
package com.imooc.user.controller;

import com.imooc.api.BaseController;
import com.imooc.api.controller.user.MyFansControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.user.service.MyFansService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
    final static Logger logger = LoggerFactory.getLogger(MyFansController.class);

    @Autowired
    private MyFansService myFansService;

    @Override
    public GraceJSONResult isMeFollowThisWriter(String writerId, String fanId) {
        boolean res = myFansService.isMeFollowThisWriter(writerId,fanId);
        return GraceJSONResult.ok(res);
    }

    @Override
    public GraceJSONResult follow(String writerId, String fanId) {
        myFansService.follow(writerId,fanId);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult unfollow(String writerId, String fanId) {
        myFansService.unfollow(writerId, fanId);
        return GraceJSONResult.ok();
    }
}
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

public interface MyFansService {
    /**
     * 查询当前用户是否关注作家
     */
    public boolean isMeFollowThisWriter(String writerId, String fanId);

    /**
     * 关注成为粉丝
     */
    public void follow(String writerId, String fanId);
    
    /**
     * 粉丝取消关注
     */
    public void unfollow(String writerId, String fanId);
}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
package com.imooc.user.service.impl;

import com.github.pagehelper.PageHelper;
import com.imooc.api.service.BaseService;
import com.imooc.enums.UserStatus;
import com.imooc.pojo.AppUser;
import com.imooc.pojo.Fans;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.mapper.FansMapper;
import com.imooc.user.service.AppUserMngService;
import com.imooc.user.service.MyFansService;
import com.imooc.utils.PagedGridResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Date;
import java.util.List;

@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
    @Autowired
    public FansMapper fansMapper;
    @Override
    public boolean isMeFollowThisWriter(String writerId, String fanId) {
        Fans fan = new Fans();
        fan.setFanId(fanId);
        fan.setWriterId(writerId);
        int count = fansMapper.selectCount(fan); //前期先放在数据库里
        return count > 0 ? true : false;
    }
    
    @Transactional
    @Override
    public void follow(String writerId, String fanId) {
        // 获得粉丝用户的信息
        AppUser fanInfo = userService.getUser(fanId);

        String fanPkId = sid.nextShort();

        Fans fans = new Fans();
        fans.setId(fanPkId);
        fans.setFanId(fanId);
        fans.setWriterId(writerId);

        fans.setFace(fanInfo.getFace());
        fans.setFanNickname(fanInfo.getNickname());
        fans.setSex(fanInfo.getSex());
        fans.setProvince(fanInfo.getProvince());

        fansMapper.insert(fans);

        // redis 作家粉丝数累加
        redis.increment(REDIS_WRITER_FANS_COUNTS + ":" + writerId, 1);  //增加key一次
        // redis 当前用户的(我的)关注数累加
        redis.increment(REDIS_MY_FOLLOW_COUNTS + ":" + fanId, 1);  //增加key一次
    }
    
    @Transactional
    @Override
    public void unfollow(String writerId, String fanId) {
        Fans fans = new Fans();
        fans.setWriterId(writerId);
        fans.setFanId(fanId);

        fansMapper.delete(fans);

        // redis 作家粉丝数累减
        redis.decrement(REDIS_WRITER_FANS_COUNTS + ":" + writerId, 1);  //增加key一次
        // redis 当前用户的(我的)关注数累减
        redis.decrement(REDIS_MY_FOLLOW_COUNTS + ":" + fanId, 1);  //增加key一次
    }
}
service-api  com/imooc/api/service/BaseService.java
package com.imooc.api.service;

import com.github.pagehelper.PageInfo;
import com.imooc.utils.PagedGridResult;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

public class BaseService {
    public static final String REDIS_ALL_CATEGORY = "redis_all_category";

    public static final String REDIS_WRITER_FANS_COUNTS = "redis_writer_fans_counts";
    public static final String REDIS_MY_FOLLOW_COUNTS = "redis_my_follow_counts";

    public static final String REDIS_ARTICLE_COMMENT_COUNTS = "redis_article_comment_counts";

    @Autowired
    public RedisOperator redis;
    public PagedGridResult setterPagedGrid(List<?> list, Integer page){ //类型是? 后期不确定是什么泛型
        PageInfo<?> pageList = new PageInfo<>(list);
        PagedGridResult gridResult = new PagedGridResult();
        gridResult.setRows(list);
        gridResult.setPage(page);
        gridResult.setRecords(pageList.getTotal());
        gridResult.setTotal(pageList.getPages());
        return gridResult;
    }
}
service-api  com/imooc/api/config/InterceptorConfig.java //增加粉丝接口的拦截
package com.imooc.api.config;

import com.imooc.api.interceptors.AdminTokenInterceptor;
import com.imooc.api.interceptors.PassportInterceptor;
import com.imooc.api.interceptors.UserActiveInterceptor;
import com.imooc.api.interceptors.UserTokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PassportInterceptor passportInterceptor(){
        return new PassportInterceptor();
    }
    @Bean
    public UserTokenInterceptor userTokenInterceptor(){
        return new UserTokenInterceptor();
    }
    @Bean
    public UserActiveInterceptor userActiveInterceptor() {
        return new UserActiveInterceptor();
    }

    @Bean
    public AdminTokenInterceptor adminTokenInterceptor() {
        return new AdminTokenInterceptor();
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
        registry.addInterceptor(passportInterceptor())
                .addPathPatterns("/passport/getSMSCode"); //拦截PassportControllerApi里的信息
        registry.addInterceptor(userTokenInterceptor())
                .addPathPatterns("/user/getAccountInfo")
                .addPathPatterns("/user/updateUserId")
                .addPathPatterns("/fs/uploadFace")
                .addPathPatterns("/fs/uploadSomeFiles")
                .addPathPatterns("/fans/follow")
                .addPathPatterns("/fans/unfollow");

        registry.addInterceptor(adminTokenInterceptor())//继续添加拦截器:查询admin列表 创建新admin用户
                .addPathPatterns("/adminMng/adminIsExist")
                .addPathPatterns("/adminMng/addNewAdmin")
                .addPathPatterns("/adminMng/getAdminList")
                .addPathPatterns("/fs/uploadToGridFS")
                .addPathPatterns("/friendLinkMng/saveOrUpdateFriendLink")
                .addPathPatterns("/friendLinkMng/getFriendLinkList")
                .addPathPatterns("/friendLinkMng/delete")
                .addPathPatterns("/categoryMng/saveOrUpdateCategory")
                .addPathPatterns("/categoryMng/getCatList");

        registry.addInterceptor(userActiveInterceptor())
                .addPathPatterns("/fs/uploadSomeFiles")
                .addPathPatterns("/fans/follow")
                .addPathPatterns("/fans/unfollow");
    }
}

粉丝数与关注数页面显示【粉丝关注】

service-user  com/imooc/user/controller/UserController.java
@RestController
public class UserController extends BaseController implements UserControllerApi {
 @Override
    public GraceJSONResult getUserInfo(String userId) {
        //接口进行解耦!!
        // 0. 判断参数不为空
        if (StringUtils.isBlank(userId)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);
        }
        // 1. 根据userId查询用户的信息 UserService+impl
        AppUser user = getUser(userId);
        // 2. 返回用户信息
        AppUserVO userVO = new AppUserVO();
        BeanUtils.copyProperties(user, userVO); //拷贝信息
        // 3. 查询redis中用户的关注数和粉丝数,放入userVO放入前端渲染
        userVO.setMyFansCounts(getCountsFromRedis(REDIS_WRITER_FANS_COUNTS + ":" + userId));
        userVO.setMyFollowCounts(getCountsFromRedis(REDIS_MY_FOLLOW_COUNTS + ":" + userId));
        return GraceJSONResult.ok(userVO);
    }
}
service-api  com/imooc/api/BaseController.java
public abstract class BaseController {
public Integer getCountsFromRedis(String key){
        String countsStr = redis.get(key);
        if (StringUtils.isBlank(countsStr)) {
            countsStr = "0";
        }
        return Integer.valueOf(countsStr);
    }
}
dev-model  com/imooc/pojo/vo/AppUserVO.java
public class AppUserVO {
    private String id;
    private String nickname;
    private String face;
    private Integer activeStatus;

    private Integer myFollowCounts;
    private Integer myFansCounts;
}Getter + Setter

我的粉丝列表_后端分页查询【粉丝管理】

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
@Api(value = "粉丝管理",tags = {"粉丝管理功能的controller"})
@RequestMapping("fans")
public interface MyFansControllerApi {
@ApiOperation(value = "查询我的所有粉丝列表", notes = "查询我的所有粉丝列表", httpMethod = "POST")
    @PostMapping("/queryAll")
    public GraceJSONResult queryAll(
            @RequestParam String writerId,
            @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
            @RequestParam Integer page,
            @ApiParam(name = "pageSize", value = "分页查询每一页显示的条数", required = false)
            @RequestParam Integer pageSize);
}
service-user  com/imooc/user/controller/MyFansController.java
@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
 @Override
    public GraceJSONResult queryAll(String writerId, Integer page, Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        return GraceJSONResult.ok(myFansService.queryMyFansList(writerId, page, pageSize));
    }
}
===================================================================
http://writer.imoocnews.com:9090/imooc-news/writer/myFans.html
数据库中fans表
weiter_id
可以改成登录的cookie里面的 uid:240629F21AK1BHX4
就可以测试用户粉丝数量
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

import com.imooc.utils.PagedGridResult;

public interface MyFansService {
    /**
     * 查询当前用户是否关注作家
     */
    public boolean isMeFollowThisWriter(String writerId, String fanId);

    /**
     * 关注成为粉丝
     */
    public void follow(String writerId, String fanId);

    /**
     * 粉丝取消关注
     */
    public void unfollow(String writerId, String fanId);

    /**
     * 查询我的粉丝
     */
    public PagedGridResult queryMyFansList(String writerId, Integer page, Integer pageSize);
}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
@Override
    public PagedGridResult queryMyFansList(String writerId, Integer page, Integer pageSize) {
        Fans fans = new Fans();
        fans.setWriterId(writerId);

        PageHelper.startPage(page,pageSize); //进行分页
        List<Fans> list = fansMapper.select(fans);
        return setterPagedGrid(list,page);
    }
}

男女比例柱状图_饼状图显示【数据可视化-粉丝画像】Echarts

Apache ECharts + 前端 [myFansCharts-static.html + myFansCharts.html]

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
@Api(value = "粉丝管理",tags = {"粉丝管理功能的controller"})
@RequestMapping("fans")
public interface MyFansControllerApi {
@ApiOperation(value = "查询男女粉丝数量", notes = "查询男女粉丝数量", httpMethod = "POST")
    @PostMapping("/queryRatio")
    public GraceJSONResult queryRatio(@RequestParam String writerId);
}
service-user  com/imooc/user/controller/MyFansController.java
@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
 @Override
    public GraceJSONResult queryRatio(String writerId) {
        int manCount = myFansService.queryFansCounts(writerId, Sex.man);
        int womanCount = myFansService.queryFansCounts(writerId, Sex.woman);

        FansCountsVO fansCountsVO = new FansCountsVO();
        fansCountsVO.setManCounts(manCount);
        fansCountsVO.setWomanCounts(womanCount);
        return GraceJSONResult.ok(fansCountsVO);
    }
}
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

import com.imooc.utils.PagedGridResult;

public interface MyFansService {
     /**
     * 查询粉丝数
     */
    public Integer queryFansCounts(String writerId, Sex sex);
}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
@Override
    public Integer queryFansCounts(String writerId, Sex sex) {
        Fans fans = new Fans();
        fans.setWriterId(writerId);
        fans.setSex(sex.type);

        int count = fansMapper.selectCount(fans);
        return count;
    }
}

中国地图粉丝地域分布数量展示【数据可视化-粉丝画像】

男女比例柱状图_饼状图显示【数据可视化-粉丝画像】Echarts

Apache ECharts + 前端 [myFansCharts-static.html + myFansCharts.html]

service-api  com/imooc/api/controller/user/MyFansControllerApi.java
@ApiOperation(value = "根据地域查询粉丝数量", notes = "根据地域查询粉丝数量", httpMethod = "POST")
    @PostMapping("/queryRatioByRegion")
    public GraceJSONResult queryRatioByRegion(@RequestParam String writerId);
}
service-user  com/imooc/user/controller/MyFansController.java
@RestController
public class MyFansController extends BaseController implements MyFansControllerApi {
  @Override
    public GraceJSONResult queryRatioByRegion(String writerId) {
        return GraceJSONResult.ok(myFansService.queryRegionRatioCounts(writerId));
    }
}
=====================================================================
将fans里的writer_id【自己的cookie里的uid 属于自己的属性 对应着右面的province省份】
service-user  com/imooc/user/service/MyFansService.java
package com.imooc.user.service;

import com.imooc.utils.PagedGridResult;

public interface MyFansService {
     /**
     * 查询粉丝数
     */
    public List<RegionRatioVO> queryRegionRatioCounts(String writerId);
}
service-user  com/imooc/user/service/impl/MyFansServiceImpl.java
@Service
public class MyFansServiceImpl extends BaseService implements MyFansService {
@Override
    public List<RegionRatioVO> queryRegionRatioCounts(String writerId) {
        Fans fans = new Fans();
        fans.setWriterId(writerId);

        List<RegionRatioVO> list = new ArrayList<>();
        for (String r : regions) {
            fans.setProvince(r);
            Integer count = fansMapper.selectCount(fans);

            RegionRatioVO regionRatioVO = new RegionRatioVO();
            regionRatioVO.setName(r);
            regionRatioVO.setValue(count);

            list.add(regionRatioVO);
        }
        return list;
    }

    public static final String[] regions = {"北京", "天津", "上海", "重庆",
            "河北", "山西", "辽宁", "吉林", "黑龙江", "江苏", "浙江", "安徽", "福建", "江西", "山东",
            "河南", "湖北", "湖南", "广东", "海南", "四川", "贵州", "云南", "陕西", "甘肃", "青海", "台湾",
            "内蒙古", "广西", "西藏", "宁夏", "新疆",
            "香港", "澳门"};
}
dev-model  com/imooc/pojo/vo/RegionRatioVO.java
package com.imooc.pojo.vo;

public class RegionRatioVO {

    private String name;
    private Integer value;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }
}

开发文章详情接口 【章节概述】detail.html

  • 文章详情页
  • 文章评论模块
  • 评论管理

文章详情页

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
 @GetMapping("detail")
    @ApiOperation(value = "文章详情查询", notes = "文章详情查询", httpMethod = "GET")
    public GraceJSONResult detail(@RequestParam String articleId);
service-article  com/imooc/article/controller/ArticlePortalController.java
 @Override
    public GraceJSONResult detail(String articleId) {
        ArticleDetailVO detailVO = articlePortalService.queryDetail(articleId);

        Set<String> idSet = new HashSet();
        idSet.add(detailVO.getPublishUserId());
        List<AppUserVO> publisherList = getPublisherList(idSet);

        if (!publisherList.isEmpty()) {
            detailVO.setPublishUserName(publisherList.get(0).getNickname());
        }

        detailVO.setReadCounts(
                getCountsFromRedis(REDIS_ARTICLE_READ_COUNTS + ":" + articleId));

        return GraceJSONResult.ok(detailVO);
    }
==================================================================
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=240721DDAHBPWG0H
service-article  com/imooc/article/service/ArticlePortalService.java
    /**
     * 查询文章详情
     */
    public ArticleDetailVO queryDetail(String articleId);
service-article  com/imooc/article/service/impl/ArticlePortalServiceImpl.java
    @Override
    public ArticleDetailVO queryDetail(String articleId) {
        Article article = new Article();
        article.setId(articleId);
        article.setIsAppoint(YesOrNo.NO.type);
        article.setIsDelete(YesOrNo.NO.type);
        article.setArticleStatus(ArticleReviewStatus.SUCCESS.type);

        Article result = articleMapper.selectOne(article);
        ArticleDetailVO detailVO = new ArticleDetailVO();
        BeanUtils.copyProperties(result, detailVO);
        return detailVO;
    }
dev-model  com/imooc/pojo/vo/ArticleDetailVO.java
package com.imooc.pojo.vo;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.util.Date;

public class ArticleDetailVO {

    private String id;
    private String title;
    private String cover;
    private Integer categoryId;
    private String categoryName;
    private String publishUserId;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date publishTime;
    private String content;

    private String publishUserName;
    private Integer readCounts;
}Getter + Setter

阅读文章_阅读量redis累加【详情页】

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@PostMapping("readArticle")
    @ApiOperation(value = "阅读文章,文章阅读量累加", notes = "阅读文章,文章阅读量累加", httpMethod = "POST")
    public GraceJSONResult readArticle(@RequestParam String articleId);
service-article  com/imooc/article/controller/ArticlePortalController.java
  @Override
    public GraceJSONResult detail(String articleId) {
        ArticleDetailVO detailVO = articlePortalService.queryDetail(articleId);

        Set<String> idSet = new HashSet();
        idSet.add(detailVO.getPublishUserId());
        List<AppUserVO> publisherList = getPublisherList(idSet);

        if (!publisherList.isEmpty()) {
            detailVO.setPublishUserName(publisherList.get(0).getNickname());
        }

        detailVO.setReadCounts( //去redis获取值 关联到前端阅读量增加 关联!!!
                getCountsFromRedis(REDIS_ARTICLE_READ_COUNTS + ":" + articleId));

        return GraceJSONResult.ok(detailVO);
    }

    @Override
    public GraceJSONResult readArticle(String articleId) {
        redis.increment(REDIS_ARTICLE_READ_COUNTS + ":" + articleId, 1);
        return GraceJSONResult.ok();
    }
==================================================================
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=240721DDAHBPWG0H
service-article  com/imooc/article/service/ArticlePortalService.java
public class ArticleDetailVO {

    private String id;
    private String title;
    private String cover;
    private Integer categoryId;
    private String categoryName;
    private String publishUserId;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date publishTime;
    private String content;

    private String publishUserName;
    private Integer readCounts;
}Getter + Setter

文章阅读数防刷策略【详情页】

限定id去做增加 readArticle中增加拦截器
在ArticlePortalControllerApi.java中的readArticle接口 增加 HttpServletRequest request

service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@PostMapping("readArticle")
    @ApiOperation(value = "阅读文章,文章阅读量累加", notes = "阅读文章,文章阅读量累加", httpMethod = "POST")
    public GraceJSONResult readArticle(@RequestParam String articleId, HttpServletRequest request);
service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public GraceJSONResult readArticle(String articleId, HttpServletRequest request) {
        String userIp = IPUtil.getRequestIp(request);
        // 设置针对当前用户ip的永久存在的key,存入redis,表示该ip的用户已经阅读过了 防刷策略
        redis.setnx(REDIS_ALREADY_READ + ":" + articleId + ":" + userIp, userIp);

        redis.increment(REDIS_ARTICLE_READ_COUNTS + ":" + articleId, 1);
        return GraceJSONResult.ok();
    }
service-api  com/imooc/api/interceptors/ArticleReadInterceptor.java //【增加拦截器】
package com.imooc.api.interceptors;

import com.imooc.utils.IPUtil;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ArticleReadInterceptor extends BaseInterceptor implements HandlerInterceptor {

    @Autowired
    public RedisOperator redis;
    public static final String REDIS_ALREADY_READ = "redis_already_read";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String articleId = request.getParameter("articleId");

        String userIp = IPUtil.getRequestIp(request);
        boolean isExist = redis.keyIsExist(REDIS_ALREADY_READ + ":" +  articleId + ":" + userIp);

        if (isExist) {
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
service-api  com/imooc/api/config/InterceptorConfig.java
package com.imooc.api.config;

import com.imooc.api.interceptors.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
...
    @Bean
    public ArticleReadInterceptor articleReadInterceptor(){
        return new ArticleReadInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry){//注册拦截器
...
        registry.addInterceptor(articleReadInterceptor())
                .addPathPatterns("/portal/article/readArticle");
    }
}

Redis mget 批量查询组装阅读量并展示【文章列表】

Redis get单个读取 && Redis mget批量读取

service-article  com/imooc/article/controller/ArticlePortalController.java
 private PagedGridResult rebuildArticleGrid(PagedGridResult gridResult) {
        // START
        List<Article> list = (List<Article>)gridResult.getRows();
        // 1. 构建发布者id列表
        Set<String> idSet = new HashSet<>();
        List<String> idList = new ArrayList<>();
        for (Article a : list) {
//            System.out.println(a.getPublishUserId());
            // 1.1 构建发布者的set
            idSet.add(a.getPublishUserId());
            // 1.2 构建文章id的list 包含所有key的值
            idList.add(REDIS_ARTICLE_READ_COUNTS + ":" + a.getId());
        }
        System.out.println(idSet.toString());
        // 发起redis的mget批量查询api,获得对应的值
        List<String> readCountsRedisList = redis.mget(idList);
        List<AppUserVO> publisherList = getPublisherList(idSet);

        // 3. 拼接两个list,重组文章列表
        List<IndexArticleVO> indexArticleList = new ArrayList<>();
        for (int i = 0 ; i < list.size() ; i ++) {
            IndexArticleVO indexArticleVO = new IndexArticleVO();
            Article a = list.get(i); //属性值拷贝
            BeanUtils.copyProperties(a, indexArticleVO);

            // 3.1 从publisherList中获得发布者的基本信息
            AppUserVO publisher  = getUserIfPublisher(a.getPublishUserId(), publisherList);
            indexArticleVO.setPublisherVO(publisher);

            // 3.2 重新组装设置文章列表中的阅读量
            String redisCountsStr = readCountsRedisList.get(i);
            int readCounts = 0;
            if (StringUtils.isNotBlank(redisCountsStr)) {
                readCounts = Integer.valueOf(redisCountsStr);
            }
            indexArticleVO.setReadCounts(readCounts);

            indexArticleList.add(indexArticleVO);
        }
        gridResult.setRows(indexArticleList);
// END
        return gridResult;
    }

用户发表评论【文章评论】

mybatis-generator-database generatorConfig-article.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <!-- 通用mapper所在目录 -->
        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="com.imooc.my.mapper.MyMapper"/>
        </plugin>

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/imooc-news-dev"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!-- 对应生成的pojo所在包 -->
        <javaModelGenerator targetPackage="com.imooc.pojo" targetProject="mybatis-generator-database/src/main/java"/>

        <!-- 对应生成的mapper所在目录 -->
        <sqlMapGenerator targetPackage="mapper.article" targetProject="mybatis-generator-database/src/main/resources"/>

        <!-- 配置mapper对应的java映射 -->
        <javaClientGenerator targetPackage="com.imooc.article.mapper" targetProject="mybatis-generator-database/src/main/java" type="XMLMAPPER"/>

        <!-- 数据库表 -->
        <table tableName="comments"></table>

    </context>
</generatorConfiguration>
mybatis-generator-database  com/imooc/mybatis/utils/ArticleGenerator.java //【运行】
package com.imooc.mybatis.utils;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.File;
import java.util.ArrayList;
import java.util.List;


public class ArticleGenerator {

    public void generator() throws Exception {

        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        //指定 逆向工程配置文件
        File configFile = new File("mybatis-generator-database"
                                            + File.separator
                                            + "generatorConfig-article.xml");
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
                callback, warnings);
        myBatisGenerator.generate(null);

    } 
    
    public static void main(String[] args) throws Exception {
        try {
            ArticleGenerator generatorSqlmap = new ArticleGenerator();
            generatorSqlmap.generator();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
service-article  com/imooc/article/mapper/CommentsMapper.java
package com.imooc.article.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Comments;

public interface CommentsMapper extends MyMapper<Comments> {
}

================================================================

service-article  resources/mapper/CommentsMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.article.mapper.CommentsMapper">
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="writer_id" jdbcType="VARCHAR" property="writerId" />
    <result column="father_id" jdbcType="VARCHAR" property="fatherId" />
    <result column="article_id" jdbcType="VARCHAR" property="articleId" />
    <result column="article_title" jdbcType="VARCHAR" property="articleTitle" />
    <result column="article_cover" jdbcType="VARCHAR" property="articleCover" />
    <result column="comment_user_id" jdbcType="VARCHAR" property="commentUserId" />
    <result column="comment_user_nickname" jdbcType="VARCHAR" property="commentUserNickname" />
    <result column="comment_user_face" jdbcType="VARCHAR" property="commentUserFace" />
    <result column="content" jdbcType="VARCHAR" property="content" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
  </resultMap>
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="writer_id" jdbcType="VARCHAR" property="writerId" />
    <result column="father_id" jdbcType="VARCHAR" property="fatherId" />
    <result column="article_id" jdbcType="VARCHAR" property="articleId" />
    <result column="article_title" jdbcType="VARCHAR" property="articleTitle" />
    <result column="article_cover" jdbcType="VARCHAR" property="articleCover" />
    <result column="comment_user_id" jdbcType="VARCHAR" property="commentUserId" />
    <result column="comment_user_nickname" jdbcType="VARCHAR" property="commentUserNickname" />
    <result column="comment_user_face" jdbcType="VARCHAR" property="commentUserFace" />
    <result column="content" jdbcType="VARCHAR" property="content" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
  </resultMap>
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="writer_id" jdbcType="VARCHAR" property="writerId" />
    <result column="father_id" jdbcType="VARCHAR" property="fatherId" />
    <result column="article_id" jdbcType="VARCHAR" property="articleId" />
    <result column="article_title" jdbcType="VARCHAR" property="articleTitle" />
    <result column="article_cover" jdbcType="VARCHAR" property="articleCover" />
    <result column="comment_user_id" jdbcType="VARCHAR" property="commentUserId" />
    <result column="comment_user_nickname" jdbcType="VARCHAR" property="commentUserNickname" />
    <result column="content" jdbcType="VARCHAR" property="content" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
  </resultMap>
</mapper>
service-api  com/imooc/api/controller/article/CommentControllerApi.java
@Api(value = "评论相关业务的controller", tags = {"评论相关业务的controller"})
@RequestMapping("comment")
public interface CommentControllerApi {

    @PostMapping("createComment")
    @ApiOperation(value = "用户评论", notes = "用户评论", httpMethod = "POST") //@Valid是做验证的
    public GraceJSONResult createArticle(@RequestBody @Valid CommentReplyBO commentReplyBO, BindingResult result);
service-article  com/imooc/article/controller/CommentController.java
@RestController
public class CommentController extends BaseController implements CommentControllerApi {
    final static Logger logger = LoggerFactory.getLogger(CommentController.class);

    @Override
    public GraceJSONResult createArticle(@Valid CommentReplyBO commentReplyBO,
                                         BindingResult result) {
        // 0. 判断BindingResult是否保存错误的验证信息,如果有,则直接return
        if (result.hasErrors()) {
            Map<String, String> errorMap = getErrors(result);
            return GraceJSONResult.errorMap(errorMap);
        }

        // 1. 根据留言用户的id查询他的昵称,用于存入到数据表进行字段的冗余处理,从而避免多表关联查询的性能影响
        String userId = commentReplyBO.getCommentUserId();

        // 2. 发起restTemplate调用用户服务,获得用户侧昵称
        Set<String> idSet = new HashSet<>();
        idSet.add(userId);
        String nickname = getBasicUserList(idSet).get(0).getNickname();
...[未完待续]
service-api  com/imooc/api/BaseController.java
public List<AppUserVO> getBasicUserList(Set idSet) {
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> userVOList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            userVOList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return userVOList;
    }
dev-model  com/imooc/pojo/bo/CommentReplyBO.java
/**
 * 文章留言的BO
 */
public class CommentReplyBO {

    @NotBlank(message = "留言信息不完整")
    private String articleId;

    @NotBlank(message = "留言信息不完整")
    private String fatherId;

    @NotBlank(message = "当前用户信息不正确,请尝试重新登录")
    private String commentUserId;

    @NotBlank(message = "留言内容不能为空")
    @Length(max = 50, message = "文章内容长度不能超过50")
    private String content;
}Getter + Setter + ToString
dev-model  com/imooc/pojo/Comments.java
public class Comments {
    @Id
    private String id;

    /**
     * 评论的文章是哪个作者的关联id
     */
    @Column(name = "writer_id")
    private String writerId;

    /**
     * 如果是回复留言,则本条为子留言,需要关联查询
     */
    @Column(name = "father_id")
    private String fatherId;

    /**
     * 回复的那个文章id
     */
    @Column(name = "article_id")
    private String articleId;

    /**
     * 冗余文章标题,宽表处理,非规范化的sql思维,对于几百万文章和几百万评论的关联查询来讲,性能肯定不行,所以做宽表处理,从业务角度来说,文章发布以后不能随便修改标题和封面的
     */
    @Column(name = "article_title")
    private String articleTitle;

    /**
     * 文章封面
     */
    @Column(name = "article_cover")
    private String articleCover;

    /**
     * 发布留言的用户id
     */
    @Column(name = "comment_user_id")
    private String commentUserId;

    /**
     * 冗余用户昵称,非一致性字段,用户修改昵称后可以不用同步
     */
    @Column(name = "comment_user_nickname")
    private String commentUserNickname;

    /**
     * 冗余的用户头像
     */
    @Column(name = "comment_user_face")
    private String commentUserFace;

    /**
     * 留言内容
     */
    private String content;

    /**
     * 留言时间
     */
    @Column(name = "create_time")
    private Date createTime;

用户评论入库保存【文章评论】这里暂时把数据库的comment_user_face删除了

service-api  com/imooc/api/controller/article/CommentControllerApi.java
@Api(value = "评论相关业务的controller", tags = {"评论相关业务的controller"})
@RequestMapping("comment")
public interface CommentControllerApi {

    @PostMapping("createComment")
    @ApiOperation(value = "用户评论", notes = "用户评论", httpMethod = "POST") //@Valid是做验证的
    public GraceJSONResult createArticle(@RequestBody @Valid CommentReplyBO commentReplyBO, BindingResult result);
}
service-article  com/imooc/article/controller/CommentController.java
@RestController
public class CommentController extends BaseController implements CommentControllerApi {
    final static Logger logger = LoggerFactory.getLogger(CommentController.class);
    @Autowired
    private CommentPortalService commentPortalService;

    @Override
    public GraceJSONResult createArticle(@Valid CommentReplyBO commentReplyBO,
                                         BindingResult result) {
        // 0. 判断BindingResult是否保存错误的验证信息,如果有,则直接return
        if (result.hasErrors()) {
            Map<String, String> errorMap = getErrors(result);
            return GraceJSONResult.errorMap(errorMap);
        }

        // 1. 根据留言用户的id查询他的昵称,用于存入到数据表进行字段的冗余处理,从而避免多表关联查询的性能影响
        String userId = commentReplyBO.getCommentUserId();

        // 2. 发起restTemplate调用用户服务,获得用户侧昵称
        Set<String> idSet = new HashSet<>();
        idSet.add(userId);
        String nickname = getBasicUserList(idSet).get(0).getNickname();

        // 3. 保存用户评论的信息到数据库
        commentPortalService.createComment(commentReplyBO.getArticleId(), commentReplyBO.getFatherId(), commentReplyBO.getContent(), userId, nickname);

        return GraceJSONResult.ok();
    }
}
service-article  com/imooc/article/service/CommentPortalService.java
public interface CommentPortalService {
    /**
     * 发表评论
     */
    public void createComment(String articleId,
                              String fatherCommentId,
                              String content,
                              String userId,
                              String nickname);
}
service-article  com/imooc/article/service/impl/CommentPortalServiceImpl.java
package com.imooc.article.service.impl;

import com.imooc.api.service.BaseService;
import com.imooc.article.mapper.CommentsMapper;
import com.imooc.article.service.ArticlePortalService;
import com.imooc.article.service.CommentPortalService;
import com.imooc.pojo.Comments;
import com.imooc.pojo.vo.ArticleDetailVO;
import com.imooc.utils.PagedGridResult;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;


@Service
public class CommentPortalServiceImpl extends BaseService implements CommentPortalService {
    @Autowired
    private ArticlePortalService articlePortalService;
    @Autowired
    private Sid sid;
    @Autowired
    private CommentsMapper commentsMapper;

    @Transactional
    @Override
    public void createComment(String articleId, String fatherCommentId, String content, String userId, String nickname) {
        String commentId = sid.nextShort();
        ArticleDetailVO article = articlePortalService.queryDetail(articleId);
        Comments comments = new Comments();
        comments.setId(commentId);

        comments.setWriterId(article.getPublishUserId());
        comments.setArticleTitle(article.getTitle());
        comments.setArticleCover(article.getCover());
        comments.setArticleId(articleId);

        comments.setFatherId(fatherCommentId);
        comments.setCommentUserId(userId);
        comments.setCommentUserNickname(nickname);

        comments.setContent(content);
        comments.setCreateTime(new Date());

        commentsMapper.insert(comments);

        // 评论数累加
        redis.increment(REDIS_ARTICLE_COMMENT_COUNTS + ":" + articleId, 1);
    }
}
dev-model  com/imooc/pojo/Comments.java
public class Comments {
    @Id
    private String id;

    /**
     * 评论的文章是哪个作者的关联id
     */
    @Column(name = "writer_id")
    private String writerId;

    /**
     * 如果是回复留言,则本条为子留言,需要关联查询
     */
    @Column(name = "father_id")
    private String fatherId;

    /**
     * 回复的那个文章id
     */
    @Column(name = "article_id")
    private String articleId;

    /**
     * 冗余文章标题,宽表处理,非规范化的sql思维,对于几百万文章和几百万评论的关联查询来讲,性能肯定不行,所以做宽表处理,从业务角度来说,文章发布以后不能随便修改标题和封面的
     */
    @Column(name = "article_title")
    private String articleTitle;

    /**
     * 文章封面
     */
    @Column(name = "article_cover")
    private String articleCover;

    /**
     * 发布留言的用户id
     */
    @Column(name = "comment_user_id")
    private String commentUserId;

    /**
     * 冗余用户昵称,非一致性字段,用户修改昵称后可以不用同步
     */
    @Column(name = "comment_user_nickname")
    private String commentUserNickname;

//    /**
//     * 冗余的用户头像
//     */
//    @Column(name = "comment_user_face")
//    private String commentUserFace;

    /**
     * 留言内容
     */
    private String content;

    /**
     * 留言时间
     */
    @Column(name = "create_time")
    private Date createTime;
}
service-api  com/imooc/api/BaseController.java
public List<AppUserVO> getBasicUserList(Set idSet) {
        String userServerUrlExecute
                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> userVOList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            userVOList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return userVOList;
    }
service-article  com/imooc/article/mapper/CommentsMapper.java
package com.imooc.article.mapper;

import com.imooc.my.mapper.MyMapper;
import com.imooc.pojo.Comments;
import org.springframework.stereotype.Repository;

@Repository
public interface CommentsMapper extends MyMapper<Comments> {
}
service-article  resources/mapper/CommentsMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="writer_id" property="writerId" jdbcType="VARCHAR" />
    <result column="father_id" property="fatherId" jdbcType="VARCHAR" />
    <result column="article_id" property="articleId" jdbcType="VARCHAR" />
    <result column="article_title" property="articleTitle" jdbcType="VARCHAR" />
    <result column="article_cover" property="articleCover" jdbcType="VARCHAR" />
    <result column="comment_user_id" property="commentUserId" jdbcType="VARCHAR" />
    <result column="comment_user_nickname" property="commentUserNickname" jdbcType="VARCHAR" />
    <result column="content" property="content" jdbcType="VARCHAR" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
  </resultMap>
</mapper>

评论数累计与显示【文章评论】

service-api  com/imooc/api/controller/article/CommentControllerApi.java
@Api(value = "评论相关业务的controller", tags = {"评论相关业务的controller"})
@RequestMapping("comment")
public interface CommentControllerApi {
@GetMapping("counts")
    @ApiOperation(value = "用户评论数查询", notes = "用户评论数查询", httpMethod = "GET")
    public GraceJSONResult commentCounts(@RequestParam String articleId);
}
service-article  com/imooc/article/controller/CommentController.java
 @Override
    public GraceJSONResult commentCounts(String articleId) {
        Integer counts = getCountsFromRedis(REDIS_ARTICLE_COMMENT_COUNTS + ":" + articleId);
        return GraceJSONResult.ok(counts);
    }

--------------------------------------------------------------------------
service-api  com/imooc/api/BaseController.java
public Integer getCountsFromRedis(String key){
        String countsStr = redis.get(key);
        if (StringUtils.isBlank(countsStr)) {
            countsStr = "0";
        }
        return Integer.valueOf(countsStr);
    }

文章评论sql关联查询father_id…

【多表关联查询】
SELECT
    c.id as commentId,
    c.father_id as fatherId,
    c.comment_user_id as commentUserId,
    c.comment_user_nickname as commentUserNickname,
    c.article_id as articleId,
    c.content as content,
    c.create_time as createTime,
    f.comment_user_nickname as quoteUserNickname,
    f.content as quoteContent
FROM
    comments c
LEFT JOIN
    comments f
ON
    c.father_id = f.id
WHERE
    c.article_id = '2006117B57WRZGHH'
ORDER BY
    c.create_time
DESC

显示评论列表【文章评论】

service-api  com/imooc/api/controller/article/CommentControllerApi.java
com/imooc/api/controller/article/CommentControllerApi.java 
@GetMapping("list")
    @ApiOperation(value = "查询文章的所有评论列表", notes = "查询文章的所有评论列表", httpMethod = "GET")
    public GraceJSONResult list(@RequestParam String articleId,
                                @RequestParam Integer page,
                                @RequestParam Integer pageSize);
service-article  com/imooc/article/controller/CommentController.java
@Override
    public GraceJSONResult list(String articleId, Integer page, Integer pageSize) {
        if (page == null) {
            page = COMMON_START_PAGE;
        }

        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }
        PagedGridResult gridResult = commentPortalService.queryArticleComments(articleId, page, pageSize);
        return GraceJSONResult.ok(gridResult);
    }
==============================================================
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=200816961ZYBXFRP
service-article  com/imooc/article/service/CommentPortalService.java
/**
     * 查询文章评论列表
     */
    public PagedGridResult queryArticleComments(String articleId,
                                                Integer page,
                                                Integer pageSize);
service-article  com/imooc/article/mapper/CommentsMapperCustom.java
package com.imooc.article.mapper;

import com.imooc.pojo.vo.CommentsVO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Map;

@Repository
public interface CommentsMapperCustom {

    /**
     * 查询文章评论
     */
    public List<CommentsVO> queryArticleCommentList(@Param("paramMap") Map<String, Object> map);

}
service-article  com/imooc/article/service/impl/CommentPortalServiceImpl.java
@Override
    public PagedGridResult queryArticleComments(String articleId, Integer page, Integer pageSize) {
        Map<String, Object> map = new HashMap<>();
        map.put("articleId", articleId);

        PageHelper.startPage(page, pageSize);
        List<CommentsVO> list = commentsMapperCustom.queryArticleCommentList(map);
        return setterPagedGrid(list,page);
    }
service-article  resources/mapper/CommentsMapperCustom.xml #【把关于face的字段都删掉】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapperCustom" >

  <select id="queryArticleCommentList"
          resultType="com.imooc.pojo.vo.CommentsVO"
          parameterType="Map">

    SELECT
      c.id as commentId,
      c.father_id as fatherId,
      c.comment_user_id as commentUserId,
      c.comment_user_nickname as commentUserNickname,
      c.article_id as articleId,
      c.content as content,
      c.create_time as createTime,
      f.comment_user_nickname as quoteUserNickname,
      f.content as quoteContent
    FROM
      comments c
        LEFT JOIN
      comments f
      ON
        c.father_id = f.id
    WHERE
      c.article_id = #{paramMap.articleId}
    ORDER BY
      c.create_time
            DESC

  </select>
</mapper>

(作业) 管理评论列表以及删除评论【评论管理】

前端的commentMng.html的VUE有问题
需要增加定义userInfo
var mainPage = new Vue({
el: “#mainPage”,
data: {
userInfo: {
activeStatus: 0
},
}…

service-api  com/imooc/api/controller/article/CommentControllerApi.java
    @PostMapping("mng")
    @ApiOperation(value = "查询我的评论管理列表", notes = "查询我的评论管理列表", httpMethod = "POST")
    public GraceJSONResult mng(@RequestParam String writerId,
                               @ApiParam(name = "page", value = "查询下一页的第几页", required = false)
                               @RequestParam Integer page,
                               @ApiParam(name = "pageSize", value = "分页的每一页显示的条数", required = false)
                               @RequestParam Integer pageSize);


    @PostMapping("/delete")
    @ApiOperation(value = "作者删除评论", notes = "作者删除评论", httpMethod = "POST")
    public GraceJSONResult delete(@RequestParam String writerId,
                                  @RequestParam String commentId);
service-article  com/imooc/article/controller/CommentController.java
 @Override
    public GraceJSONResult mng(String writerId, Integer page, Integer pageSize) {

        if (page == null) {
            page = COMMON_START_PAGE;
        }
        if (pageSize == null) {
            pageSize = COMMON_PAGE_SIZE;
        }

        PagedGridResult gridResult = commentPortalService.queryWriterCommentsMng(writerId, page, pageSize);
        return GraceJSONResult.ok(gridResult);
    }

    @Override
    public GraceJSONResult delete(String writerId, String commentId) {
        commentPortalService.deleteComment(writerId, commentId);
        return GraceJSONResult.ok();
    }
==============================================================
http://writer.imoocnews.com:9090/imooc-news/writer/commentMng.html
service-article  com/imooc/article/service/CommentPortalService.java
     /**
     * 查询我的评论管理列表
     */
    public PagedGridResult queryWriterCommentsMng(String writerId, Integer page, Integer pageSize);

    /**
     * 删除评论
     */
    public void deleteComment(String writerId, String commentId);
service-article  com/imooc/article/service/impl/CommentPortalServiceImpl.java
@Override
    public PagedGridResult queryWriterCommentsMng(String writerId, Integer page, Integer pageSize) {
        Comments comment = new Comments();
        comment.setWriterId(writerId);
        PageHelper.startPage(page, pageSize);
        List<Comments> list = commentsMapper.select(comment);
        return setterPagedGrid(list,page);
    }

    @Override
    public void deleteComment(String writerId, String commentId) {
        Comments comment = new Comments();
        comment.setId(commentId);
        comment.setWriterId(writerId);
        commentsMapper.delete(comment);
    }

增加评论者头像展示功能需求扩展【文章评论】增加需求字段comment_user_face

[数据库添加一个新的字段comment_user_face 重新在mybatis-generator-database进行逆向生成覆盖]
涉及范围广

在数据库里也要加个字段 在前端需求也要改一下头像

detail.html
<div class="all-comments-list" v-for="(comment,index) in commentList" :key="index">
        <div class="single-comment-wrapper">
        <!--<img src="./img/face1.png" class="user-face"/>-->
        <img :src="comment.commentUserFace" class="user-face"/>
</div>
service-article  resources/mapper/CommentsMapper.xml #【增加字段】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapper" >
  <resultMap id="BaseResultMap" type="com.imooc.pojo.Comments" >
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" property="id" jdbcType="VARCHAR" />
    <result column="writer_id" property="writerId" jdbcType="VARCHAR" />
    <result column="father_id" property="fatherId" jdbcType="VARCHAR" />
    <result column="article_id" property="articleId" jdbcType="VARCHAR" />
    <result column="article_title" property="articleTitle" jdbcType="VARCHAR" />
    <result column="article_cover" property="articleCover" jdbcType="VARCHAR" />
    <result column="comment_user_id" property="commentUserId" jdbcType="VARCHAR" />
    <result column="comment_user_nickname" property="commentUserNickname" jdbcType="VARCHAR" />
    <result column="comment_user_face" property="commentUserFace" jdbcType="VARCHAR" />
    <result column="content" property="content" jdbcType="VARCHAR" />
    <result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
  </resultMap>
</mapper>
service-article  resources/mapper/CommentsMapperCustom.xml #【增加字段】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imooc.article.mapper.CommentsMapperCustom" >

  <select id="queryArticleCommentList"
          resultType="com.imooc.pojo.vo.CommentsVO"
          parameterType="Map">

    SELECT
      c.id as commentId,
      c.father_id as fatherId,
      c.comment_user_id as commentUserId,
      c.comment_user_nickname as commentUserNickname,
      c.comment_user_face as commentUserFace,
      c.article_id as articleId,
      c.content as content,
      c.create_time as createTime,
      f.comment_user_nickname as quoteUserNickname,
      f.content as quoteContent
    FROM
      comments c
        LEFT JOIN
      comments f
      ON
        c.father_id = f.id
    WHERE
      c.article_id = #{paramMap.articleId}
    ORDER BY
      c.create_time
            DESC

  </select>

</mapper>
service-article  com/imooc/article/service/CommentPortalService.java //【增加字段】
 /**
     * 发表评论
     */
    public void createComment(String articleId,
                              String fatherCommentId,
                              String content,
                              String userId,
                              String nickname,
                              String face);
------------------------------------------------------------
dev-model  com/imooc/pojo/vo/CommentsVO.java //【增加字段属性】
private String commentUserFace;
    【Getter + Setter】

------------------------------------------------------------
dev-model  com/imooc/pojo/Comments.java
/**
     * 冗余的用户头像
     */
@Column(name = "comment_user_face")
private String commentUserFace;
    【Getter + Setter】
------------------------------------------------------------
service-article  com/imooc/article/controller/CommentController.java 
    //【增加 String face = getBasicUserList(idSet).get(0).getFace();】
@Override
    public GraceJSONResult createArticle(@Valid CommentReplyBO commentReplyBO,
                                         BindingResult result) {
        // 0. 判断BindingResult是否保存错误的验证信息,如果有,则直接return
        if (result.hasErrors()) {
            Map<String, String> errorMap = getErrors(result);
            return GraceJSONResult.errorMap(errorMap);
        }

        // 1. 根据留言用户的id查询他的昵称,用于存入到数据表进行字段的冗余处理,从而避免多表关联查询的性能影响
        String userId = commentReplyBO.getCommentUserId();

        // 2. 发起restTemplate调用用户服务,获得用户侧昵称
        Set<String> idSet = new HashSet<>();
        idSet.add(userId);
        String nickname = getBasicUserList(idSet).get(0).getNickname();
        String face = getBasicUserList(idSet).get(0).getFace();

        // 3. 保存用户评论的信息到数据库
        commentPortalService.createComment(commentReplyBO.getArticleId(), commentReplyBO.getFatherId(), commentReplyBO.getContent(), userId, nickname,face);

        return GraceJSONResult.ok();
    }
====================================================
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=2006116Z3MAP8SW0
//下面有个评论:牛逼  带着自己上传的头像
service-article  com/imooc/article/service/impl/CommentPortalServiceImpl.java
    //【增加 comments.setCommentUserFace(face);】
@Transactional
    @Override
    public void createComment(String articleId, String fatherCommentId, String content, String userId, String nickname,String face) {
        String commentId = sid.nextShort();
        ArticleDetailVO article = articlePortalService.queryDetail(articleId);
        Comments comments = new Comments();
        comments.setId(commentId);

        comments.setWriterId(article.getPublishUserId());
        comments.setArticleTitle(article.getTitle());
        comments.setArticleCover(article.getCover());
        comments.setArticleId(articleId);

        comments.setFatherId(fatherCommentId);
        comments.setCommentUserId(userId);
        comments.setCommentUserNickname(nickname);
        comments.setCommentUserFace(face);

        comments.setContent(content);
        comments.setCreateTime(new Date());

        commentsMapper.insert(comments);

        // 评论数累加
        redis.increment(REDIS_ARTICLE_COMMENT_COUNTS + ":" + articleId, 1);
    }

文章静态化技术与Freemarker【文章概述】

  • 页面静态化
  • Freemarker静态化技术
  • 渲染模板数据
  • 生成并展示静态页面
静态化趋势
  • 便于SEO
  • 加速用户访问
  • 降低数据库压力
模板引擎技术
  • JSP
  • Freemarker
  • Thymeleaf
  • Velocity
页面静态化

创建并且显示模板ftl

service-article  pom.xml
 <!-- freemarker 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
service-article  com/imooc/article/controller/FreemarkerController.java
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
@RequestMapping("free")
public class FreemarkerController{
    @GetMapping("/hello")
    public String hello(Model model){
        // 定义输出到模板的内容
        // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);

        // 返回的stu是freemarker模板所在的目录 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }
}
==================================================================
http://localhost:8001/free/hello
service-article  resources/application.yml 【suffix是模板后缀】
  freemarker:
    charset: UTF-8
    content-type: text/html
    suffix: .ftl
    template-loader-path: classpath:/templates/
service-article  resources/templates/stu.ftl
<html>
    <head>
        <title>Hello Freemarker</title>
    </head>
    <body>
    <#-- 
        写完以后去模板页面配置 application.yml
        Freemarker 页面的语法构成:
        1. 注释
        2. 表达式 ${...}
        3. 普通文本,基本的html标签
        4. 指令
    -->
        <div>
            hello ${there}
        </div>
    </body>
</html>

输出对象【Freemarker语法】

dev-model  com/imooc/pojo/Stu.java
public class Stu {
    private String uid;
    private String username;
    private Integer age;
    private Date birthday;
    private Float amount;
    private boolean haveChild;
    private Spouse spouse;
} Getter + Setter
dev-model  com/imooc/pojo/Spouse.java
public class Spouse {
    private String username;
    private Integer age;
} Getter + Setter
service-article  com/imooc/article/controller/FreemarkerController.java
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
@RequestMapping("free")
public class FreemarkerController{
    @GetMapping("/hello")
    public String hello(Model model){
        // 定义输出到模板的内容
        // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);

        makeModel(model);
        // 返回的stu是freemarker模板所在的目录 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }

    private Model makeModel(Model model) {
        Stu stu = new Stu();
        stu.setUid("10010");
        stu.setUsername("imooc");
        stu.setAmount(88.86f);
        stu.setAge(18);
        stu.setHaveChild(true);
        stu.setBirthday(new Date());

        Spouse spouse = new Spouse();
        spouse.setUsername("Lucy");
        spouse.setAge(25);
        stu.setSpouse(spouse);
        model.addAttribute("stu",stu);
        return model;
    }
}
==================================================================
http://localhost:8001/free/hello

Hello 慕课网 imooc.com

用户名uid: 10010
用户姓名: imooc
年龄:18
生日:2024-07-29 15:13:05
用户余额:88.86
已育:yes
伴侣:Lucy,25岁
service-article  resources/templates/stu.ftl
<html>
<head>
    <title>Hello Freemarker</title>
</head>
<body>
<#-- 写完以后去模板页面配置 application.yml
    Freemarker 页面的语法构成:
    1. 注释
    2. 表达式 ${...}
    3. 普通文本,基本的html标签
    4. 指令
-->
    <div>
        hello ${there}
    </div>
<br>

    <div>
        用户名uid: ${stu.uid}<br>
        用户姓名: ${stu.username}<br>
        年龄:${stu.age}<br>
        生日:${stu.birthday?string('yyyy-MM-dd HH:mm:ss')}<br> <#-- 日期转换 -->
        用户余额:${stu.amount}<br>
        已育:${stu.haveChild?string('yes', 'no')}<br>
        伴侣:${stu.spouse.username},${stu.spouse.age}岁

    </div>
</body>
</html>

输出list与map【Freemarker语法】

dev-model  com/imooc/pojo/Stu.java
public class Stu {
    private String uid;
    private String username;
    private Integer age;
    private Date birthday;
    private Float amount;
    private boolean haveChild;

    private Spouse spouse;

    private List<Article> articleList;
    private Map<String, String> parents;
} Getter + Setter
dev-model  com/imooc/pojo/Spouse.java
public class Spouse {
    private String username;
    private Integer age;
} Getter + Setter
service-article  com/imooc/article/controller/FreemarkerController.java
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.Article;
import com.imooc.pojo.Spouse;
import com.imooc.pojo.Stu;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;

@Controller
@RequestMapping("free")
public class FreemarkerController{
    @GetMapping("/hello")
    public String hello(Model model){
        // 定义输出到模板的内容
        // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);

        makeModel(model);
        // 返回的stu是freemarker模板所在的目录 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }

    private Model makeModel(Model model) {
        Stu stu = new Stu();
        stu.setUid("10010");
        stu.setUsername("imooc");
        stu.setAmount(88.86f);
        stu.setAge(18);
        stu.setHaveChild(true);
        stu.setBirthday(new Date());

        Spouse spouse = new Spouse();
        spouse.setUsername("Lucy");
        spouse.setAge(25);

        stu.setSpouse(spouse);
        stu.setArticleList(getArticles());
        stu.setParents(getParents());

        model.addAttribute("stu",stu);
        return model;
    }

    private List<Article> getArticles(){
        Article article1 = new Article();
        article1.setId("1001");
        article1.setTitle("今天天气不错");

        Article article2 = new Article();
        article2.setId("1002");
        article2.setTitle("今天下雨了");

        Article article3 = new Article();
        article3.setId("1003");
        article3.setTitle("昨天下雨了");

        List<Article> list = new ArrayList<>();
        list.add(article1);
        list.add(article2);
        list.add(article3);
        return list;
    }

    private Map<String, String> getParents(){
        Map<String, String> parents = new HashMap<>();
        parents.put("father", "XiaoMing");
        parents.put("mother", "LiLi");
        return parents;
    }
}
==================================================================
http://localhost:8001/free/hello

hello 慕课网 imooc.com

用户名uid: 10010
用户姓名: imooc
年龄:18
生日:2024-07-29 15:39:07
用户余额:88.86
已育:yes
伴侣:Lucy,25岁

1001 今天天气不错
1002 今天下雨了
1003 昨天下雨了

LiLi
XiaoMing
service-article  resources/templates/stu.ftl
<html>
<head>
    <title>Hello Freemarker</title>
</head>
<body>
<#-- 写完以后去模板页面配置 application.yml
    Freemarker 页面的语法构成:
    1. 注释
    2. 表达式 ${...}
    3. 普通文本,基本的html标签
    4. 指令
-->
    <div>
        hello ${there}
    </div>
<br>

    <div>
        用户名uid: ${stu.uid}<br>
        用户姓名: ${stu.username}<br>
        年龄:${stu.age}<br>
        生日:${stu.birthday?string('yyyy-MM-dd HH:mm:ss')}<br> <#-- 日期转换 -->
        用户余额:${stu.amount}<br>
        已育:${stu.haveChild?string('yes', 'no')}<br>
        伴侣:${stu.spouse.username},${stu.spouse.age}岁
    </div>

<br>

    <div>
        <#list stu.articleList as article>
            <div>
                <span>${article.id}</span>
                <span>${article.title}</span>
            </div>
        </#list>
    </div>

<br>

    <div>
        <#list stu.parents?keys as key>
            <div>
                ${stu.parents[key]}
            </div>
        </#list>
    </div>
</body>
</html>

指令if【Freemarker语法】

service-article  resources/templates/stu.ftl
<html>
<head>
    <title>Hello Freemarker</title>
</head>
<body>
<#-- 
    写完以后去模板页面配置 application.yml
    Freemarker 页面的语法构成:
    1. 注释
    2. 表达式 ${...}
    3. 普通文本,基本的html标签
    4. 指令
-->
    <div>
        hello ${there}
    </div>
<br>

    <div>
        用户名uid: ${stu.uid}<br>
        用户姓名: ${stu.username}<br>
        年龄:${stu.age}<br>
        生日:${stu.birthday?string('yyyy-MM-dd HH:mm:ss')}<br> <#-- 日期转换 -->
        用户余额:${stu.amount}<br>
        已育:${stu.haveChild?string('yes', 'no')}<br>
        <#if stu.spouse??>
            伴侣:${stu.spouse.username}, ${stu.spouse.age}岁
        </#if>
        <#if !stu.spouse??>
            单身狗
        </#if>
    </div>

<br>

    <div>
        <#list stu.articleList as article>
            <div>
                <span>${article.id}</span>
                <span>${article.title}</span>
            </div>
        </#list>
    </div>

<br>

    <div>
        <#list stu.parents?keys as key>
            <div>
                ${stu.parents[key]}
            </div>
        </#list>
    </div>

<br>

    <div>
        <#if stu.uid == '10010'>
            用户id是10010
            <br>
        </#if>
        <#if stu.username != 'imooc'>
            用户名不是imooc
            <br>
        </#if>
        <#if (stu.age >= 18) >
            用户已成年
        </#if>
        <br>
        <#if (stu.age > 18 || stu.age = 18) >
            成年人
            <br>
        </#if>
        <#if (stu.age < 18) >
            未成年
            <br>
        </#if>
        <#if stu.haveChild >
            已育
        </#if>
        <br>
        <#if !stu.haveChild >
             未育
        </#if>
     </div>
</body>
</html>

在这里特别注意一下 已经开始第二阶段的代码 进阶篇 所以前端的代码也是需要更新换代的 包括../js/app.js里面多了 app.getPageName();
没有getPageName这个函数-慕课网 (imooc.com)
生成的html调用app.js中getPageName()函数出错的问题-慕课网 (imooc.com)

结合动态数据生成静态化HTML【Freemarker】

service-article  com/imooc/article/controller/FreemarkerController.java
【stu.ftl如上图不变增加java的整合代码】 俗称Java+ftl=HTML
package com.imooc.article.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.Article;
import com.imooc.pojo.Spouse;
import com.imooc.pojo.Stu;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.*;

@Controller
@RequestMapping("free")
public class FreemarkerController{

    @Value("${freemarker.html.target}")
    private String htmlTarget;

    @GetMapping("/createHTML")
    @ResponseBody
    public String createHTML(Model model) throws IOException, TemplateException {
        // 0. 配置freemarker基本环境
        Configuration cfg = new Configuration(Configuration.getVersion());
        // 声明freemarker模板所需要加载的目录的位置
            //resources/templates/stu.ftl
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File((classpath + "templates")));

            // 测试打印
        System.out.println(htmlTarget);
        System.out.println(classpath + "templates");
        /**
         * /workspace/freemarker_html
         * /C:/Users/Pluminary/Desktop/backup/imooc-news-dev/imooc-news-dev-service-article/target/classes/templates
         */
        // 1. 获得现有的模板ftl文件
        Template template = cfg.getTemplate("stu.ftl", "utf-8");

        // 2. 获得动态数据
            // 定义输出到模板的内容
            // 输入字符串
        String stranger = "慕课网 imooc.com";
        model.addAttribute("there", stranger);
        model = makeModel(model);

        // 3. 融合动态数据和ftl,生成html
        File tempDic = new File(htmlTarget);
        if (!tempDic.exists()) {
            tempDic.mkdirs();
        }
        Writer out = new FileWriter(htmlTarget + File.separator + "10010" + ".html");
        template.process(model, out);
        out.close();
        return "ok";
        // C:\workspace\freemarker_html\10010.html 里面的数据都是静态数据
    }

    @GetMapping("/hello")
    public String hello(Model model){
        makeModel(model);
        // 返回的stu是freemarker模板所在的目录 classpath:/templates/
        // 匹配 *.ftl
        return "stu";
    }

    private Model makeModel(Model model) {
        Stu stu = new Stu();
        stu.setUid("10010");
        stu.setUsername("imooc");
        stu.setAmount(88.86f);
        stu.setAge(18);
        stu.setHaveChild(true);
        stu.setBirthday(new Date());

        Spouse spouse = new Spouse();
        spouse.setUsername("Lucy");
        spouse.setAge(25);

        stu.setSpouse(spouse);
        stu.setArticleList(getArticles());
        stu.setParents(getParents());

        model.addAttribute("stu",stu);
        return model;
    }

    private List<Article> getArticles(){
        Article article1 = new Article();
        article1.setId("1001");
        article1.setTitle("今天天气不错");

        Article article2 = new Article();
        article2.setId("1002");
        article2.setTitle("今天下雨了");

        Article article3 = new Article();
        article3.setId("1003");
        article3.setTitle("昨天下雨了");

        List<Article> list = new ArrayList<>();
        list.add(article1);
        list.add(article2);
        list.add(article3);
        return list;
    }

    private Map<String, String> getParents(){
        Map<String, String> parents = new HashMap<>();
        parents.put("father", "XiaoMing");
        parents.put("mother", "LiLi");
        return parents;
    }
}

改写详情页为模板页ftl【页面静态化】

地址页不是拼接 将detail.html 的路径都变成绝对路径

<link rel="shortcut icon" href="../img/mu-toutiao.ico" />

把其拷贝到 resources/templates/detail.ftl

【放在d盘 tomcat/webapps中的】detail.ftl
<div class="big-title">
     ${articleDetail.title}
</div>
    <div class="read-counts" v-show="articleDetail.readCounts != '' && articleDetail.readCounts != null">
     阅读量:${articleDetail.readCounts}
    </div>
......
<div class="date-title">
       <span class="year">${articleDetail.publishTime?string('yyyy')}</span>
 </div>
      <div class="back-year-line"></div>

      <div class="date-md">${articleDetail.publishTime?string('MM/dd')}</div>

      <div class="date-times">${articleDetail.publishTime?string('HH:mm:ss')}</div>

      <div class="writer-name" @click="showWriter('${articleDetail.publishUserId}')">
                        ${articleDetail.publishUserName}
                    </div>
.....
<div class="article-wrapper">
   <div class="content">
        ${articleDetail.content}
   </div>
<div class="declare">
      免责声明:本平台所有内容仅供测试,且文章来自互联网,不代表慕课网的观点和立场,如有不妥,请联系后删除。
    </div>
</div>

文章详情ftl生成静态化页面【页面静态化】

freemarker:
  html:
    target: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
    article: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
dev-model  com/imooc/pojo/vo/ArticleDetailVO.java
public class ArticleDetailVO {

    private String id;
    private String title;
    private String cover;
    private Integer categoryId;
    private String categoryName;
    private String publishUserId;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date publishTime;
    private String content;

    private String publishUserName;
    private Integer readCounts;
}Getter + Setter
service-article  com/imooc/article/controller/ArticleController.java
 @Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);
        if (pendingStatus == ArticleReviewStatus.SUCCESS.type){
            //审核成功,生成文章详情页静态html
            try{
                 createArticleHTML(articleId);
//                String articleMongoId = createArticleHTMLToGridF(articleId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return GraceJSONResult.ok();
    }

    @Value("${freemarker.html.target}")
    private String articlePath;
    @Autowired
    private RestTemplate restTemplate;
    // 文章生成HTML
    public void createArticleHTML(String articleId) throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.getVersion());
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File(classpath + "templates"));

        Template template = cfg.getTemplate("detail.ftl", "utf-8");

        // 获得文章的详情数据
        ArticleDetailVO detailVO = getArticleDetail(articleId);
        Map<String, Object> map = new HashMap<>();
        map.put("articleDetail", detailVO);

        File tempDic = new File(articlePath);
        if (!tempDic.exists()) {
            tempDic.mkdirs();
        }

        String path = articlePath + File.separator + detailVO.getId() + ".html";

        Writer out = new FileWriter(path);
        template.process(map, out);
        out.close();
    }
    // 发起远程调用rest,获得文章详情数据
    public ArticleDetailVO getArticleDetail(String articleId) {
        String url
                = "http://www.imoocnews.com:8001/portal/article/detail?articleId=" + articleId;
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(url, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        ArticleDetailVO detailVO = null;
        if (bodyResult.getStatus() == 200) {
            String detailJson = JsonUtils.objectToJson(bodyResult.getData());
            detailVO = JsonUtils.jsonToPojo(detailJson, ArticleDetailVO.class);
        }
        return detailVO;
    }
先去发表头条http://writer.imoocnews.com:9090/imooc-news/writer/createArticle.html
再去审核通过http://www.imoocnews.com:9090/imooc-news/admin/contentReview.html
此时运行后 就会有java+ftl=html静态页面在指定位置生成了
指定位置:D:\apache-tomcat-8.5.93\webapps\imooc-news\portal\a
生成了一个文件:240729D9S8683XP0.html  这里面就是刚刚发表头条的内容
此时下面的两个网站都可以打开同样的头条内容
http://www.imoocnews.com:9090/imooc-news/portal/a/240729D9S8683XP0.html
http://www.imoocnews.com:9090/imooc-news/portal/detail.html?articleId=240729D9S8683XP0

240729D9S8683XP0.html
<div class="writer-name" @click="showWriter('240629F21AK1BHX4')">
      P_luminary
</div>

// 跳转作家页面
showWriter(writerId) {
    window.open("../writer.html?writerId=" + writerId);
},

没有getPageName这个函数-慕课网 (imooc.com)

文章阅读量detail单独获取并展示 【页面静态化】

a5 (imoocnews.com)

【去前面代入阅读量】
 <div class="read-counts">
            阅读量:{{readCounts}}
        </div>
【先定义readCounts初始量为0】
var articleList = new Vue({
        el: "#detailContainer",
        data: {
            nowReplyingFatherCommentId: 0,  // 根据当前用户正在回复的父commentId进行页面的留言看展示或隐藏
            userInfo: null,
            
            articleId: "",
            articleDetail: {},
            readCounts: 0,
        }

// 获得文章阅读数
         this.getArticleReadCounts(articleId);

 // 获得文章阅读数
        getArticleReadCounts(articleId) {
               var me = this;

               var articleServerUrl = app.articleServerUrl;
               axios.defaults.withCredentials = true;
               axios.get(articleServerUrl + "/portal/article/readCounts?articleId=" + articleId)
                .then(res => {
                  // console.log(JSON.stringify(res.data));
                  this.readCounts = res.data;
                });
            },
//★★★★★★★★★★★★★★★★★★★★     ★★★★★★★★★★★★★★★★★★★★★★★\\
然后把这个临时页面修改的地方 移动到后端service-article  resources/templates/detail.ftl中
service-article  com/imooc/article/controller/ArticlePortalController.java
@Override
    public Integer readCounts(String articleId) {
       return getCountsFromRedis(REDIS_ARTICLE_READ_COUNTS + ":" + articleId);
    }
service-api  com/imooc/api/controller/article/ArticlePortalControllerApi.java
@GetMapping("readCounts")
    @ApiOperation(value = "获得文章阅读数", notes = "获得文章阅读数", httpMethod = "GET")
    public Integer readCounts(@RequestParam String articleId);

在这里折腾一天终于好了 文章:a6 (imoocnews.com)

9-10

梳理生产端消费端与中间gridfs关系

静态化高度耦合

在本地电脑/同一台服务器 => 生成静态页面HTML →(发布) 前端

解耦静态化

后端服务器(生成静态页面HTML) => GridFS <= 前端服务器(前端HTML)
同时 后端服务器(生成静态页面HTML) → 前端服务器(前端HTML)

创建一个新的module => imooc-news-dev-service-article-html
把service-article中resources的application.yml /dev+prod.yml logback-spring.xml拷贝到article-html模块的resources中

############################################################
http://localhost:8002/hello
{
    "status": 200,
    "msg": "操作成功!",
    "success": true,
    "data": null
}
application.yml
############################################################
#
# article文章静态化服务
# web访问端口号  约定:8002
#
############################################################
server:
# port: 8003
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  profiles:
    active: dev # yml中配置文件的环境配置, dev:开发环境, test:测试环境, prod:生产环境
  application:
    name: service-article-html
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

  data:
    mongodb:
      uri: mongodb://root:root@192.168.170.135:27017
      database: imooc-news
  freemarker:
    charset: UTF-8
    content-type: text/html
    suffix: .ftl
    template-loader-path: classpath:/templates/

# 定义freemarker生成的HTML
freemarker:
  html:
    target: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
    article: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
application-dev.yml
server:
  port: 8002

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379

## setup CN from java, This is resource
website:
  domain-name: imoocnews.com




application-prod.yml
server:
  port: 8002

spring:
  redis:
    database: 0
    host: 47.98.225.105
    port: 6379
service-article-html  com/imooc/article/html/Application.java
package com.imooc.article.html;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-article-html  com/imooc/article/html/controller/HelloController.java
package com.imooc.article.html.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);

    public Object hello() {
        return GraceJSONResult.ok();
    }
}

生产端存储html道gridfs并关联文章表【静态化解耦】

静态化解耦步骤
  • 生成html,并上传到gridfs中
  • 获得mongoFileId,关联保存到文章表中
  • 调用消费端,下载gridfs的html进行发布
service-article  com/imooc/article/service/ArticleService.java
 /**
     * 关联文章和gridfs的html文件id
     */
    public void updateArticleToGridFS(String articleId, String articleMongoId);
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
 @Transactional
    @Override
    public void updateArticleToGridFS(String articleId, String articleMongoId) {
        Article pendingArticle = new Article();
        pendingArticle.setId(articleId);
        pendingArticle.setMongoFileId(articleMongoId);
        articleMapper.updateByPrimaryKeySelective(pendingArticle);
    }
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);

        if (pendingStatus == ArticleReviewStatus.SUCCESS.type){
            //审核成功,生成文章详情页静态html
            try{
//                 createArticleHTML(articleId);
                String articleMongoId = createArticleHTMLToGridFS(articleId);
                // 存储到对应的文章 进行关联保存
                articleService.updateArticleToGridFS(articleId, articleMongoId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return GraceJSONResult.ok();
    }
......
...
    @Value("${freemarker.html.target}")
    private String articlePath;
    @Autowired
    private RestTemplate restTemplate;
    // 文章生成HTML
    public void createArticleHTML(String articleId) throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.getVersion());
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File(classpath + "templates"));

        Template template = cfg.getTemplate("detail.ftl", "utf-8");

        // 获得文章的详情数据
        ArticleDetailVO detailVO = getArticleDetail(articleId);
        Map<String, Object> map = new HashMap<>();
        map.put("articleDetail", detailVO);

        File tempDic = new File(articlePath);
        if (!tempDic.exists()) {
            tempDic.mkdirs();
        }

        String path = articlePath + File.separator + detailVO.getId() + ".html";

        Writer out = new FileWriter(path);
        template.process(map, out);
        out.close();
    }

    @Autowired
    private GridFSBucket gridFSBucket;
    public String createArticleHTMLToGridFS(String articleId) throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.getVersion());
        String classpath = this.getClass().getResource("/").getPath();
        cfg.setDirectoryForTemplateLoading(new File(classpath + "templates"));

        Template template = cfg.getTemplate("detail.ftl", "utf-8");

        // 获得文章的详情数据
        ArticleDetailVO detailVO = getArticleDetail(articleId);
        Map<String, Object> map = new HashMap<>();
        map.put("articleDetail", detailVO);

        String htmlContent = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
//        System.out.println(htmlContent);

        InputStream inputStream = IOUtils.toInputStream(htmlContent);
        ObjectId fileId = gridFSBucket.uploadFromStream(detailVO.getId() + ".html",inputStream);
        return fileId.toString();
    }
service-article  com/imooc/article/GridFSConfig.java
package com.imooc.article;

import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component //可以被容器访问到
public class GridFSConfig {
    @Value("${spring.data.mongodb.database}")
    private String mongodb;

    @Bean
    public GridFSBucket gridFSBucket(MongoClient mongoClient){
        MongoDatabase mongoDatabase = mongoClient.getDatabase(mongodb);
        GridFSBucket bucket = GridFSBuckets.create(mongoDatabase);//存入mongodatabase
        return bucket;
    }
}

消费端从gridfs下载HTML到tomcat【静态化解耦】

service-api  com/imooc/api/controller/article/ArticleHTMLControllerApi.java
package com.imooc.api.controller.article;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.NewArticleBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.Date;

@Api(value = "静态化文章业务的controller", tags = {"静态化文章业务的controller"})
@RequestMapping("article/html")
public interface ArticleHTMLControllerApi {

    @GetMapping("download")
    @ApiOperation(value = "下载html", notes = "下载html", httpMethod = "GET")
    public Integer download(String articleId, String articleMongoId) throws Exception;
}
article-html  com/imooc/article/html/controller/ArticleHTMLController.java
package com.imooc.article.html.controller;

import com.imooc.api.controller.article.ArticleHTMLControllerApi;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.gridfs.GridFS;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

@RestController
public class ArticleHTMLController implements ArticleHTMLControllerApi {

    final static Logger logger = LoggerFactory.getLogger(ArticleHTMLController.class);

    @Autowired //相应的下载
    private GridFSBucket gridFSBucket;

    @Value("${freemarker.html.article}")
    private String articlePath;

    @Override
    public Integer download(String articleId, String articleMongoId)
            throws Exception {

        // 拼接最终文件的保存的地址
        String path = articlePath + File.separator + articleId + ".html";

        // 获取文件流,定义存放的位置和名称
        File file = new File(path);
        // 创建输出流
        OutputStream outputStream = new FileOutputStream(file);
        // 执行下载
        gridFSBucket.downloadToStream(new ObjectId(articleMongoId), outputStream);

        return HttpStatus.OK.value();
    }
}
service-article  com/imooc/article/controller/ArticleController.java
@Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);

        if (pendingStatus == ArticleReviewStatus.SUCCESS.type){
            //审核成功,生成文章详情页静态html
            try{
//                 createArticleHTML(articleId);
                String articleMongoId = createArticleHTMLToGridFS(articleId);
                // 存储到对应的文章 进行关联保存
                articleService.updateArticleToGridFS(articleId, articleMongoId);
                // 调用消费端,执行下载html
                doDownloadArticleHTML(articleId,articleMongoId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return GraceJSONResult.ok();
    }
    private void doDownloadArticleHTML(String articleId, String articleMongoId) {
        String url = //去SwitchHost弄个新的端口映射
                "http://html.imoocnews.com:8002/article/html/download?articleId="
                        + articleId +
                        "&articleMongoId="
                        + articleMongoId;
        ResponseEntity<Integer> responseEntity = restTemplate.getForEntity(url, Integer.class);
        int status = responseEntity.getBody();
        if (status != HttpStatus.OK.value()) {
            GraceException.display(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
    }
service-article  com/imooc/article/service/ArticleService.java
      /**
     * 关联文章和gridfs的html文件id
     */
    public void updateArticleToGridFS(String articleId, String articleMongoId);
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
    @Transactional
    @Override
    public void updateArticleToGridFS(String articleId, String articleMongoId) {
        Article pendingArticle = new Article();
        pendingArticle.setId(articleId);
        pendingArticle.setMongoFileId(articleMongoId);
        articleMapper.updateByPrimaryKeySelective(pendingArticle);
    }
【SwitchHosts】
# imooc-news 192.168.1.3
127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com
```
127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com
127.0.0.1 html.imoocnews.com

发布文章后审核文章
此时会发现数据库MongoDB里面的GridFS存储桶有新建的html 包括在 前端也存在此文件
D:\apache-tomcat-8.5.93\webapps\imooc-news\portal\a\240731CN3X1M56Y8.html

撤回删除文章,删除gridfs文件以及html【静态化解耦】

拿到mongodb_id 去删除 在service-html 写个删除接口 拼接删除方法

service-api  com/imooc/api/controller/article/ArticleHTMLControllerApi.java
@Api(value = "静态化文章业务的controller", tags = {"静态化文章业务的controller"})
@RequestMapping("article/html")
public interface ArticleHTMLControllerApi {
@GetMapping("delete")
    @ApiOperation(value = "删除html", notes = "删除html", httpMethod = "GET")
    public Integer delete(String articleId) throws Exception;
}
article-html  com/imooc/article/html/controller/ArticleHTMLController.java
@Override
    public Integer delete(String articleId) throws Exception {
        // 拼接最终文件的保存的地址
        String path = articlePath + File.separator + articleId + ".html";
        // 获取文件流,定义存放的位置和名称
        File file = new File(path);
        // 删除文件
        file.delete();
        return HttpStatus.OK.value();
    }
service-article  com/imooc/article/controller/ArticleController.java
 @Override
    public GraceJSONResult delete(String userId, String articleId) {
        articleService.deleteArticle(userId,articleId);
        return GraceJSONResult.ok();
    }

    @Override
    public GraceJSONResult withdraw(String userId, String articleId) {
        articleService.withdrawArticle(userId, articleId);
        return GraceJSONResult.ok();
    }
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
@Transactional
    @Override
    public void deleteArticle(String userId, String articleId) {
        Example articleExample = makeExampleCriteria(userId, articleId);

        Article pending = new Article();
        pending.setIsDelete(YesOrNo.YES.type);

        int result = articleMapper.updateByExampleSelective(pending, articleExample);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_DELETE_ERROR);
        }
        deleteHTML(articleId);
    }

    @Transactional
    @Override
    public void withdrawArticle(String userId, String articleId) {
        Example articleExample = makeExampleCriteria(userId, articleId);

        Article pending = new Article();
        pending.setArticleStatus(ArticleReviewStatus.WITHDRAW.type);

        int result = articleMapper.updateByExampleSelective(pending, articleExample);
        if (result != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_WITHDRAW_ERROR);
        }
        deleteHTML(articleId);
    }
...
...

    @Autowired
    private GridFSBucket gridFSBucket;
    /**
     * 文章撤回删除后,删除静态化的html
     */

    public void deleteHTML(String articleId) {
        // 1. 查询文章的mongoFileId
        Article pending = articleMapper.selectByPrimaryKey(articleId);
        String articleMongoId = pending.getMongoFileId();

        // 2. 删除GridFS上的文件
        gridFSBucket.delete(new ObjectId(articleMongoId));

        // 3. 删除消费端的HTML文件
        doDeleteArticleHTML(articleId);
//        doDeleteArticleHTMLByMQ(articleId);
    }

    @Autowired
    public RestTemplate restTemplate;
    private void doDeleteArticleHTML(String articleId) {
        String url = "http://html.imoocnews.com:8002/article/html/delete?articleId=" + articleId;
        ResponseEntity<Integer> responseEntity = restTemplate.getForEntity(url, Integer.class);
        int status = responseEntity.getBody();
        if (status != HttpStatus.OK.value()) {
            GraceException.display(ResponseStatusEnum.SYSTEM_OPERATION_ERROR);
        }
    }

接口解耦需求【章节概述】

  • 介绍RabbitMQ
  • RabbitMQ术语
  • 安装与配置消息队列
  • 实现接口调用解耦

RabbitMQ概述_MQ模型

消息队列
  • RabbitMQ
  • ActiveMQ
  • RocketMQ
  • Kafka
RabbitMQ
应用场景
  • 异步任务
  • 提速
  • 接口解耦
  • 削峰
RabbitMQ模型

RabbitMQ-3.8.5 安装与配置详细在”多线程与分布式.md“中有

E:\Java实例项目1-20套\第04套【项目实战】Spring Cloud分布式微服务实战,打造大型自媒体3大业务平台 分布式前后端分离项目分层聚合 养成应对复杂业务的综合技术能力\imooc-news\rabbitmq-server-3.8.5

rabbitmq/erlang - Installation · packagecloud- Bash Scripts

[imooc@imooc ~]$ curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash
[imooc@imooc ~]$ sudo yum install erlang
[imooc@imooc ~]$ erl
Erlang/OTP 23 [erts-11.2.2.10] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [hipe]

Eshell V11.2.2.10  (abort with ^G)
1> 
[imooc@imooc ~]$ yum list | grep erlang
erlang.x86_64                               23.3.4.11-1.el7            @rabbitmq_erlang
erlang-debuginfo.x86_64                     23.3.4.11-1.el7            rabbitmq_erlang
[imooc@imooc ~]$ sudo rpm --import https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
[imooc@imooc ~]$ sudo rpm --import https://packagecloud.io/gpg.key

将资源包里的文件拷贝过来 rabbitmq.conf 和 rabbitmq-server.rpm
#[先把两个依赖搞好 => 一个是key 一个是依赖]
[imooc@imooc ~]$ sudo rpm --import https://www.rabbitmq.com/rabbitmq-release-signing-key.asc
[imooc@imooc ~]$ sudo yum install socat
[imooc@imooc ~]$ sudo rpm -ivh rabbitmq-server-3.8.5-1.el7.noarch.rpm
[imooc@imooc ~]$ sudo vim rabbitmq.conf
#{loopback_users, []} 加上注释#
[imooc@imooc ~]$ cd /etc/rabbitmq/
#把conf移动到etc中
[imooc@imooc rabbitmq]$ sudo cp /home/imooc/rabbitmq.conf .
#重新启动rabbitmq
[imooc@imooc rabbitmq]$ sudo systemctl restart rabbitmq-server
#查看状态
[imooc@imooc rabbitmq]$ sudo systemctl status rabbitmq-server

● rabbitmq-server.service - RabbitMQ broker
   Loaded: loaded (/usr/lib/systemd/system/rabbitmq-server.service; disabled; vendor preset: disabled)
   Active: active (running) since 四 2024-08-01 15:45:54 CST; 13s ago
 Main PID: 5843 (beam.smp)
   Status: "Initialized"
    Tasks: 86
   CGroup: /system.slice/rabbitmq-server.service
           ├─5843 /usr/lib64/erlang/erts-11.2.2.10/bin/beam.smp -W w -K true -A 64 -M...
           ├─5952 erl_child_setup 32768
           ├─6008 inet_gethost 4
           └─6009 inet_gethost 4
           
[imooc@imooc rabbitmq]$ sudo rabbitmq-plugins enable rabbitmq_management
[imooc@imooc rabbitmq]$ ll
总用量 40
-rw-r--r--. 1 root rabbitmq    23 8月   1 15:46 enabled_plugins
-rw-r--r--. 1 root rabbitmq 33325 8月   1 15:35 rabbitmq.conf

http://192.168.170.135:15672/
username: guest
password: guest
[imooc@imooc ~]$ sudo vim rabbitmq.conf #把这个取消注释
loopback_users.guest = false

http://192.168.170.135:15672/#/  #创建虚拟host节点
→ admin → Add a user

引入依赖和配置【集成Rabbitmq】

service-api  pom.xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

模块 Module
imooc-news-dev-service-article 是生产者[发送消息]

imooc-news-dev-service-article-html 是消费者[处理消息]

RabbitMQ Management 在Virtual Hosts → Add a new virtual host → Name: imooc-news-dev
退出再重新登陆一下rabbitmq → 账号密码:admin

service-article  application.yml
  rabbitmq:
    host: 192.168.170.135
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev
service-article-html  application.yml
  rabbitmq:
    host: 192.168.170.135
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev

创建交换机和队列【集成Rabbitmq】

service-api  com/imooc/api/config/RabbitMQConfig.java
package com.imooc.api.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 的配置类
 */
@Configuration
public class RabbitMQConfig {

    // 定义交换机的名字
    public static final String EXCHANGE_ARTICLE = "exchange_article";

    // 定义队列的名字
    public static final String QUEUE_DOWNLOAD_HTML = "queue_download_html";

    // 创建交换机
    @Bean(EXCHANGE_ARTICLE)
    public Exchange exchange(){
        return ExchangeBuilder
                .topicExchange(EXCHANGE_ARTICLE)
                .durable(true)
                .build();
    }

    // 创建队列
    @Bean(QUEUE_DOWNLOAD_HTML)
    public Queue queue(){
        return new Queue(QUEUE_DOWNLOAD_HTML);
    }

    // 队列绑定交换机
    @Bean
    public Binding binding(
            @Qualifier(QUEUE_DOWNLOAD_HTML) Queue queue,
            @Qualifier(EXCHANGE_ARTICLE) Exchange exchange){
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                //.with("article.*") "article.hello",  //类似于API的规则
                .with("article.#.do")
                .noargs();      // 执行绑定
    }
}

创建生产者_配置路由规则【集成RabbitMQ】

localhost:8001/producer/hello

{
“status”: 200,
“msg”: “操作成功!”,
“success”: true,
“data”: null
}

//如果队列规则改变 就需要把Exchanges里的RoutingKey解绑[Unbind] 否则还是会有以前的规则收到消息
//http://192.168.170.135:15672/#/exchanges/imooc-news-dev/exchange_article 
package com.imooc.article.controller;

import com.imooc.api.config.RabbitMQConfig;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("producer")
public class HelloController{
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/hello")
    public Object hello() {
    /**
     * RabbitMQ的路由规则 routing key
     * display.*.*  →  * 代表一个占位符
     * .with("article.#.do")  //类似于API的规则
     * 例:
     *      display.do.download      匹配
     *      display.do.upload.done 不匹配
     *
     * display.# → # 代表任意多个占位符
     * 例:
     *      display.do.download      匹配
     *      display.do.upload.done.over 匹配
     */

        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.publish.download.do", //要绑定规则
                "1001~");
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.success.do", //要绑定规则
                "1002~");
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.play", //要绑定规则
                "1003~");
        return GraceJSONResult.ok();
    }
}
http://localhost:8001/producer/hello

消费者接受消息处理业务【集成RabbitMQ】

RabbitMQ Management

如果消息被消费掉后那么就 需要重新请求消息队列生成

service-article-html  com/imooc/article/html/RabbitMQConsumer.java
package com.imooc.article.html;

import com.imooc.api.config.RabbitMQConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
//打断点后 只要队列有消息 消费者监听到就会被消费
@Component
public class RabbitMQConsumer {
    @RabbitListener(queues = {RabbitMQConfig.QUEUE_DOWNLOAD_HTML})//监听哪个队列
    public void watchQueue(String payload, Message message){
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        if (routingKey.equalsIgnoreCase("article.publish.download.do")) {
            System.out.println("article.publish.download.do");
        } else if (routingKey.equalsIgnoreCase("article.success.do")) {
            System.out.println("article.success.do");
        } else {
            System.out.println("不符合的规则:" + routingKey);
        }
    }
}
==================================================================
// 如果消息被消费掉后那么就 需要重新请求消息队列生成
// 此时需要刷新 http://localhost:8001/producer/hello 重新提交一下消息就可以了
Console:
1001~
article.publish.download.do
1002~
article.success.do
1003~
不符合的规则:article.play

文章静态化HTML与删除【异步解耦】

service-article  com/imooc/article/controller/ArticleController.java
 @Override
    public GraceJSONResult doReview(String articleId, Integer passOrNot) {
        Integer pendingStatus;
        if (passOrNot == YesOrNo.YES.type) {
            // 审核成功
            pendingStatus = ArticleReviewStatus.SUCCESS.type;
        } else if (passOrNot == YesOrNo.NO.type) {
            // 审核失败
            pendingStatus = ArticleReviewStatus.FAILED.type;
        } else {
            return GraceJSONResult.errorCustom(ResponseStatusEnum.ARTICLE_REVIEW_ERROR);
        }
        // 保存到数据库,更改文章状态为审核成功或者失败
        articleService.updateArticleStatus(articleId, pendingStatus);

        if (pendingStatus == ArticleReviewStatus.SUCCESS.type){
            //审核成功,生成文章详情页静态html
            try{
//                 createArticleHTML(articleId);
                String articleMongoId = createArticleHTMLToGridFS(articleId);
                // 存储到对应的文章 进行关联保存
                articleService.updateArticleToGridFS(articleId, articleMongoId);
                // 调用消费端,执行下载html
//                doDownloadArticleHTML(articleId,articleMongoId);
            ★★  // 发送消息到mq队列,让消费者监听并且下载html  ★★
                doDownloadArticleHTMLByMQ(articleId,articleMongoId);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return GraceJSONResult.ok();
    }
@Autowired
    private RabbitTemplate rabbitTemplate;
    private void doDownloadArticleHTMLByMQ(String articleId, String articleMongoId) {

        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.download.do",
                articleId + "," + articleMongoId);
    }
service-article-html  com/imooc/article/html/controller/ArticleHTMLComponent.java
package com.imooc.article.html.controller;

import com.mongodb.client.gridfs.GridFSBucket;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

@Component
public class ArticleHTMLComponent {

    @Autowired
    private GridFSBucket gridFSBucket;

    @Value("${freemarker.html.article}")
    private String articlePath;

    public Integer download(String articleId, String articleMongoId)
            throws Exception {

        // 拼接最终文件的保存的地址
        String path = articlePath + File.separator + articleId + ".html";

        // 获取文件流,定义存放的位置和名称
        File file = new File(path);
        // 创建输出流
        OutputStream outputStream = new FileOutputStream(file);
        // 执行下载
        gridFSBucket.downloadToStream(new ObjectId(articleMongoId), outputStream);

        return HttpStatus.OK.value();
    }

    public Integer delete(String articleId) throws Exception {

        // 拼接最终文件的保存的地址
        String path = articlePath + File.separator + articleId + ".html";

        // 获取文件流,定义存放的位置和名称
        File file = new File(path);

        // 删除文件
        file.delete();

        return HttpStatus.OK.value();
    }
}
service-article-html  com/imooc/article/html/RabbitMQConsumer.java
package com.imooc.article.html;

import com.imooc.api.config.RabbitMQConfig;
import com.imooc.article.html.controller.ArticleHTMLComponent;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
//打断点后 只要队列有消息 消费者监听到就会被消费
@Component
public class RabbitMQConsumer {
    @Autowired
    private ArticleHTMLComponent articleHTMLComponent;

    @RabbitListener(queues = {RabbitMQConfig.QUEUE_DOWNLOAD_HTML})//监听哪个队列
    public void watchQueue(String payload, Message message){
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        if (routingKey.equalsIgnoreCase("article.publish.download.do")) {
            System.out.println("article.publish.download.do");
        } else if (routingKey.equalsIgnoreCase("article.success.do")) {
            System.out.println("article.success.do");
        }else if (routingKey.equalsIgnoreCase("article.download.do")) {
            String articleId = payload.split(",")[0];
            String articleMongoId = payload.split(",")[1];
            try {
                articleHTMLComponent.download(articleId, articleMongoId);
            } catch (Exception e) {
                e.printStackTrace();
            }

        } else if (routingKey.equalsIgnoreCase("article.html.download.do")) {
            String articleId = payload;
            try {
                articleHTMLComponent.delete(articleId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("不符合的规则:" + routingKey);
        }
    }
}
前端的index.html页面也需要修改成静态页面跳转
<a :href="'./a/'+article.id+'.html'" target="_blank" class="link-article-title">{{article.title}}</a>
这样再次刷新 就可以让页面不是articleId=?...
http://www.imoocnews.com:9090/imooc-news/portal/a/240801D7S7PM63R4.html

延迟队列的需求与安装配置【延迟队列】

把这个rabbitmq_delayed_message_exchange-3.8.0.ez上传到Linux虚拟机

[imooc@imooc rabbitmq]$ cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.5/plugins
[imooc@imooc ~]$ sudo mv /home/imooc/rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.5/plugins
[imooc@imooc plugins]$ sudo systemctl restart rabbitmq-server
[imooc@imooc plugins]$ sudo rabbitmq-plugins list
Listing plugins with pattern ".*" ...
 Configured: E = explicitly enabled; e = implicitly enabled
 | Status: * = running on rabbit@imooc
 |/
[  ] rabbitmq_amqp1_0                  3.8.5
[  ] rabbitmq_auth_backend_cache       3.8.5
[  ] rabbitmq_auth_backend_http        3.8.5
[  ] rabbitmq_auth_backend_ldap        3.8.5
[  ] rabbitmq_auth_backend_oauth2      3.8.5
[  ] rabbitmq_auth_mechanism_ssl       3.8.5
[  ] rabbitmq_consistent_hash_exchange 3.8.5
[  ] rabbitmq_delayed_message_exchange 3.8.0
[  ] rabbitmq_event_exchange           3.8.5
[  ] rabbitmq_federation               3.8.5
[  ] rabbitmq_federation_management    3.8.5
[  ] rabbitmq_jms_topic_exchange       3.8.5
[E*] rabbitmq_management               3.8.5
[e*] rabbitmq_management_agent         3.8.5
[  ] rabbitmq_mqtt                     3.8.5
[  ] rabbitmq_peer_discovery_aws       3.8.5
[  ] rabbitmq_peer_discovery_common    3.8.5
[  ] rabbitmq_peer_discovery_consul    3.8.5
[  ] rabbitmq_peer_discovery_etcd      3.8.5
[  ] rabbitmq_peer_discovery_k8s       3.8.5
[  ] rabbitmq_prometheus               3.8.5
[  ] rabbitmq_random_exchange          3.8.5
[  ] rabbitmq_recent_history_exchange  3.8.5
[  ] rabbitmq_sharding                 3.8.5
[  ] rabbitmq_shovel                   3.8.5
[  ] rabbitmq_shovel_management        3.8.5
[  ] rabbitmq_stomp                    3.8.5
[  ] rabbitmq_top                      3.8.5
[  ] rabbitmq_tracing                  3.8.5
[  ] rabbitmq_trust_store              3.8.5
[e*] rabbitmq_web_dispatch             3.8.5
[  ] rabbitmq_web_mqtt                 3.8.5
[  ] rabbitmq_web_mqtt_examples        3.8.5
[  ] rabbitmq_web_stomp                3.8.5
[  ] rabbitmq_web_stomp_examples       3.8.5
[imooc@imooc plugins]$ service rabbitmq-server restart #[重启服务]
Redirecting to /bin/systemctl restart rabbitmq-server.service

实现延迟队列【延迟队列】

service-api  com/imooc/api/config/RabbitMQDelayConfig.java
package com.imooc.api.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * RabbitMQ 的配置类
 */
@Configuration
public class RabbitMQDelayConfig {

    // 定义交换机的名字
    public static final String EXCHANGE_DELAY = "exchange_delay";

    // 定义队列的名字
    public static final String QUEUE_DELAY = "queue_delay";

    // 创建延迟交换机
    @Bean(EXCHANGE_DELAY)
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "topic");
        return new CustomExchange(EXCHANGE_DELAY, "x-delayed-message", true, false, args);
    }

    // 创建队列
    @Bean(QUEUE_DELAY)
    public Queue queue(){
        return new Queue(QUEUE_DELAY);
    }

    // 队列绑定交换机 ↓ binding必须要唯一
    @Bean
    public Binding delayBinding(
            @Qualifier(QUEUE_DELAY) Queue queue,
            @Qualifier(EXCHANGE_DELAY) Exchange exchange){
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("delay.#")
                .noargs();      // 执行绑定
    }
}
service-article  com/imooc/article/RabbitMQDelayConsumer.java
package com.imooc.article;

import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.article.service.ArticleService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class RabbitMQDelayConsumer {

    @Autowired
    private ArticleService articleService;

    @RabbitListener(queues = {RabbitMQDelayConfig.QUEUE_DELAY})
    public void watchQueue(String payload, Message message) {
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        System.out.println(routingKey);

        System.out.println("消费者接受的延迟消息:" + new Date());

//        // 消费者接收到定时发布的延迟消息,修改当前的文章状态为`即时发布`
//        String articleId = payload;
//        articleService.updateArticleToPublish(articleId);
    }
}
service-article  com/imooc/article/controller/HelloController.java
package com.imooc.article.controller;

import com.imooc.api.config.RabbitMQConfig;
import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
@RequestMapping("producer")
public class HelloController{
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/hello")
    public Object hello() {
    /**
     * RabbitMQ的路由规则 routing key
     * display.*.*  →  * 代表一个占位符
     * .with("article.#.do")  //类似于API的规则
     * 例:
     *      display.do.download      匹配
     *      display.do.upload.done 不匹配
     *
     * display.# → # 代表任意多个占位符
     * 例:
     *      display.do.download      匹配
     *      display.do.upload.done.over 匹配
     */

        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.publish.download.do", //要绑定规则
                "1001~");
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.success.do", //要绑定规则
                "1002~");
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_ARTICLE,
                "article.play", //要绑定规则
                "1003~");
        return GraceJSONResult.ok();
    }

    @GetMapping("/delay")
    public Object delay() {
        //重写延迟方法  【生产者】
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置消息的持久
                message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                // 设置消息延迟的时间 单位ms毫秒
                message.getMessageProperties().setDelay(5000);
                return message;
            }
        };
        rabbitTemplate.convertAndSend(RabbitMQDelayConfig.EXCHANGE_DELAY,
                "delay.demo", //要绑定规则
                "这是一条延时消息~",
                messagePostProcessor);
        System.out.println("生产者发送的延迟消息:" + new Date());
        return "OK";
    }
}
确保 rabbitmq_delayed_message_exchange 插件正确启用:

复制代码
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
sudo systemctl restart rabbitmq-server

======================================================================
http://localhost:8001/producer/delay

生产者发送的延迟消息:Thu Aug 01 20:17:30 CST 2024

这是一条延时消息~
delay.demo
消费者接受的延迟消息:Thu Aug 01 20:17:35 CST 2024

实现文章的定时延时发布【延迟队列】

service-article  com/imooc/article/service/ArticleService.java
/**
     * 更新定时发布为即使发布
     
    public void updateAppointToPublish(); **/

    /**
     * 更新单条文章为记时发布
     */
    public void updateArticleToPublish(String articleId);
service-article  com/imooc/article/service/impl/ArticleServiceImpl.java
【32-64行  89-101行】
 @Transactional
    @Override
    public void createArticle(NewArticleBO newArticleBO, Category category) {
        String articleId = sid.nextShort();

        Article article = new Article();
        BeanUtils.copyProperties(newArticleBO, article);

        article.setId(articleId);
        article.setCategoryId(category.getId());
        article.setArticleStatus(ArticleReviewStatus.REVIEWING.type);
        article.setCommentCounts(0);
        article.setReadCounts(0);

        article.setIsDelete(YesOrNo.NO.type);
        article.setCreateTime(new Date());
        article.setUpdateTime(new Date());

        if (article.getIsAppoint() == ArticleAppointType.TIMING.type) {
            article.setPublishTime(newArticleBO.getPublishTime()); //用户可以在前端选择定时发布
        } else if (article.getIsAppoint() == ArticleAppointType.IMMEDIATELY.type) {
            article.setPublishTime(new Date());
        }

        int res = articleMapper.insert(article);
        if (res != 1) {
            GraceException.display(ResponseStatusEnum.ARTICLE_CREATE_ERROR);
        }

        // 发送延迟消息到mq,计算定时发布时间和当前时间的时间差,则为往后延迟的时间
        if (article.getIsAppoint() == ArticleAppointType.TIMING.type) {

            Date endDate = newArticleBO.getPublishTime();
            Date startDate = new Date();

          int delayTimes = (int)(endDate.getTime() - startDate.getTime());

            System.out.println(DateUtil.timeBetween(startDate, endDate));

            // FIXME: 为了测试方便,写死10s
//            int delayTimes = 10 * 1000;

            MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    // 设置消息的持久
                    message.getMessageProperties()
                            .setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                    // 设置消息延迟的时间,单位ms毫秒
                    message.getMessageProperties()
                            .setDelay(delayTimes);
                    return message;
                }
            };
            rabbitTemplate.convertAndSend(
                    RabbitMQDelayConfig.EXCHANGE_DELAY,
                    "publish.delay.display",
                    articleId,
                    messagePostProcessor);

            System.out.println("延迟消息-定时发布文章:" + new Date());
        }


        /**
         * FIXME: 我们只检测正常的词汇,非正常词汇大家课后去检测
         */
        // 通过阿里智能AI实现对文章文本的自动检测(自动审核)
//        String reviewTextResult = aliTextReviewUtils.reviewTextContent(newArticleBO.getContent());
        String reviewTextResult = ArticleReviewLevel.REVIEW.type;

        if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.PASS.type)) {
            // 修改当前的文章,状态标记为审核通过
            this.updateArticleStatus(articleId, ArticleReviewStatus.SUCCESS.type);
        } else if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.REVIEW.type)) {
            // 修改当前的文章,状态标记为需要人工审核
            this.updateArticleStatus(articleId, ArticleReviewStatus.WAITING_MANUAL.type);
        } else if (reviewTextResult
                .equalsIgnoreCase(ArticleReviewLevel.BLOCK.type)) {
            // 修改当前的文章,状态标记为审核未通过
            this.updateArticleStatus(articleId, ArticleReviewStatus.FAILED.type);
        }
    }

@Transactional //添加事务[更新操作]
/** @Override
    public void updateAppointToPublish() {
        articleMapperCustom.updateAppointToPublish();
    } **/

    @Override
    public void updateArticleToPublish(String articleId) {
        Article article = new Article();
        article.setId(articleId);
        article.setIsAppoint(ArticleAppointType.IMMEDIATELY.type);
        articleMapper.updateByPrimaryKeySelective(article);
    }
service-article  com/imooc/article/RabbitMQDelayConsumer.java
package com.imooc.article;

import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.article.service.ArticleService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class RabbitMQDelayConsumer {

    @Autowired
    private ArticleService articleService;

    @RabbitListener(queues = {RabbitMQDelayConfig.QUEUE_DELAY})
    public void watchQueue(String payload, Message message) {
        System.out.println(payload);

        String routingKey = message.getMessageProperties().getReceivedRoutingKey();
        System.out.println(routingKey);

        System.out.println("消费者接受的延迟消息:" + new Date());

        // 消费者接收到定时发布的延迟消息,修改当前的文章状态为`即时发布`
        String articleId = payload;
        articleService.updateArticleToPublish(articleId);
    }
}
service-api  com/imooc/api/config/RabbitMQDelayConfig.java //【换一下绑定类型.with(...)】
package com.imooc.api.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * RabbitMQ 的配置类
 */
@Configuration
public class RabbitMQDelayConfig {

    // 定义交换机的名字
    public static final String EXCHANGE_DELAY = "exchange_delay";

    // 定义队列的名字
    public static final String QUEUE_DELAY = "queue_delay";

    // 创建延迟交换机
    @Bean(EXCHANGE_DELAY)
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "topic");
        return new CustomExchange(EXCHANGE_DELAY, "x-delayed-message", true, false, args);
    }

    // 创建队列
    @Bean(QUEUE_DELAY)
    public Queue queue(){
        return new Queue(QUEUE_DELAY);
    }

    // 队列绑定交换机 ↓ binding必须要唯一
    @Bean
    public Binding delayBinding(
            @Qualifier(QUEUE_DELAY) Queue queue,
            @Qualifier(EXCHANGE_DELAY) Exchange exchange){
        return BindingBuilder
                .bind(queue)
                .to(exchange)
                .with("publish.delay.#")
                .noargs();      // 执行绑定
    }
}
http://writer.imoocnews.com:9090/imooc-news/writer/createArticle.html 发布一篇定时文章
// 在数据库里面是article → is_appoint 是1 然后延迟3天后会变成0
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15650472]
JDBC Connection [HikariProxyConnection@441638108 wrapping org.mariadb.jdbc.MariaDbConnection@159b2e33] will be managed by Spring
==>  Preparing: INSERT INTO article ( id,title,category_id,article_type,article_cover,is_appoint,article_status,publish_user_id,publish_time,read_counts,comment_counts,mongo_file_id,is_delete,create_time,update_time,content ) VALUES( ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,? ) 
==> Parameters: 240801FNS7M8G354(String), b10(String), 14(Integer), 2(Integer), (String), 1(Integer), 1(Integer), 240629F21AK1BHX4(String), 2024-08-04 00:00:00.0(Timestamp), 0(Integer), 0(Integer), null, 0(Integer), 2024-08-01 20:36:24.971(Timestamp), 2024-08-01 20:36:24.971(Timestamp), <p>b10</p>(String)
//★ <==    Updates: 1 ★
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15650472]
//★ 2天3小时23分钟 ★
//★ 延迟消息-定时发布文章:Thu Aug 01 20:36:24 CST 2024 ★
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15650472] from current transaction
==>  Preparing: UPDATE article SET article_status = ? WHERE ( ( id = ? ) ) 
==> Parameters: 2(Integer), 240801FNS7M8G354(String)
<==    Updates: 1

互联网框架演变【微服务块】

  • 架构演变
  • 微服务入门
  • SpringCloud各个组件学习
  • 改造项目服务化

注冊中心模型

  • Eureka
  • 可以把每個服務注入到eureka,更利於管理和維護,使得服務閒通信更方便

Lilei [上户口] → 派出所 ← [上户口] HanMeimei

构建Eureka注册服务【eureka】

springcloud-eureka  com/imooc/eureka/Application.java
package com.imooc.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, MongoAutoConfiguration.class})
@EnableEurekaServer // 开启注册中心
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
http://localhost:7000/hello  #运行接口
http://localhost:7000         #运行eureka
springcloud-eureka  com/imooc/eureka/controller/HelloController.java
package com.imooc.eureka.controller;

import com.imooc.api.controller.user.HelloControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController implements HelloControllerApi {
    final static Logger logger = LoggerFactory.getLogger(HelloController.class);
    public Object hello(){
        return GraceJSONResult.ok();
    }
}
springcloud-eureka  pom.xml
<?xml version="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>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-eureka</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            </dependency>
            <!-- 其他必要的依赖 -->
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR12</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>
springcloud-eureka  logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
<!--    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/service-admin"/>-->
    <property name="LOG_HOME" value="C:/Users/Pluminary/Desktop/imooc-news-dev/springcloud-eureka"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/eureka.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
springcloud-eureka  application.yml
############################################################
#
# eureka 注册中心
# web访问端口号  约定:7000
#
############################################################
server:
  port: 7000
  tomcat:
    uri-encoding: UTF-8
    max-swallow-size: -1  # tomcat默认大小2M,超过2M的文件不会被捕获,需要调整此处大小为100MB或者-1即可

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-eureka

############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:  # eureka 实例的hostname,也可以是自定义配置hostname
    hostname: eureka
  client:  # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
http://localhost:7000/
进入了Spring Eureka
Instances currently registered with Eureka

实现用户与文章的服务注册【eureka】

关于eureka客户端启动报错UnknownHostException详细解决方法_eureka unknownhostexception-CSDN博客

# 问题:
http://eureka:7000/eureka   找不到 eureka 的服务器 IP 地址

在cmd里面
C:\Users\Pluminary>ping eureka
Ping 请求找不到主机 eureka。请检查该名称,然后重试。

# 解答:
从你的描述来看,主机eureka无法解析,这是导致服务无法注册到Eureka Server的原因。你可以通过以下方法解决这个问题:

1. 更新 hosts 文件
在你的开发机器上更新 hosts 文件以手动解析 eureka 主机名。

Windows
打开记事本以管理员身份运行。

打开文件 C:\Windows\System32\drivers\etc\hosts。

添加以下行,将 <eureka服务器的IP地址> 替换为实际的IP地址:

plaintext
复制代码
<eureka服务器的IP地址> eureka
保存并关闭文件。

=================================================================
# imooc-news 192.168.1.3
127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com
```
127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com
127.0.0.1 html.imoocnews.com
springcloud-eureka  resources/application.yml
############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # eureka 实例的hostname,可以是hostname,也可以自定义配置hostname
    hostname: eureka
  client:
    # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
service-user  resources/application.yml
上面的其余不变 底下添加eureka
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
      defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
service-user  com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient // 开启eureka client 注册到server中
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
springcloud-eureka  pom.xml
<?xml version="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>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-eureka</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

    </dependencies>

</project>
springcloud-eureka  com/imooc/user/Application.java
package com.imooc.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, MongoAutoConfiguration.class})
@EnableEurekaServer // 开启注册中心
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
====================================================================
// 先启动这个服务 再启动user的服务
随后就能看见http://localhost:7000/
里面有一个注册的服务
/*  
   Instances currently registered with Eureka
   Application    AMIs    Availability Zones    Status
★ SERVICE-USER    n/a (1)    (1)    UP (1) - localhost:service-user:8003 
*/
service-article  resources/application.yml
# 定义freemarker生成的HTML
freemarker:
  html:
    target: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a
    article: D:/apache-tomcat-8.5.93/webapps/imooc-news/portal/a

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
      defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
service-article  com/imooc/article/Application.java 
//【此时再去启动这个article服务 会发现SERVICE-ARTICLE也成功的注册到Eureka中】
package com.imooc.article;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
/*
   Application    AMIs    Availability Zones    Status
★ SERVICE-ARTICLE    n/a (1)    (1)    UP (1) - localhost:service-article:8001
★ SERVICE-USER    n/a (1)    (1)    UP (1) - localhost:service-user:8003
*/

使用AppName优化服务间的通信【eureka】

实行动态化调用 地址拼接

AppName是eureka的ApplicationId = SERVICE-USER

慕课新闻·[文章article]自媒体接口api 如果页面没有信息那就是在Swagger2.java中代码的问题
Eureka

门户端文章业务的controller → /portal/article/detail → articleId=2006117B57WRZGHH

package com.imooc.api.config;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration //Springboot啓動的時候會被掃描到并且加載
@EnableSwagger2
public class Swagger2 {

    //    http://localhost:8088/swagger-ui.html     原路径
    //    http://localhost:8088/doc.html            新路径

    // 配置swagger2核心配置 docket
    @Bean
    public Docket createRestApi() {
        Predicate<RequestHandler> adminPredicate = RequestHandlerSelectors.basePackage("com.imooc.admin.controller");
        Predicate<RequestHandler> articlePredicate = RequestHandlerSelectors.basePackage("com.imooc.article.controller");
        Predicate<RequestHandler> userPredicate = RequestHandlerSelectors.basePackage("com.imooc.user.controller");
        Predicate<RequestHandler> filesPredicate = RequestHandlerSelectors.basePackage("com.imooc.files.controller");

        return new Docket(DocumentationType.SWAGGER_2)  // 指定api类型为swagger2
                .apiInfo(apiInfo())                 // 用于定义api文档汇总信息
                .select()
//                .apis(Predicates.or(userPredicate, adminPredicate, filesPredicate))
                .apis(Predicates.or(adminPredicate, articlePredicate, userPredicate, filesPredicate))
                .paths(PathSelectors.any())         // 所有controller
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("慕课新闻·自媒体接口api")                       // 文档页标题
                .contact(new Contact("imooc",
                        "https://www.imooc.com",
                        "abc@imooc.com"))                   // 联系人信息
                .description("专为慕课新闻·自媒体平台提供的api文档")      // 详细信息
                .version("1.0.1")                               // 文档版本号
                .termsOfServiceUrl("https://www.imooc.com")     // 网站地址
                .build();
    }
}
service-article  com/imooc/article/controller/ArticlePortalController.java
// 注入服务发现,可以获得已经注册的服务相关信息
    @Autowired
    private DiscoveryClient discoveryClient;
    // 发起远程调用,获得用户的基本信息
    private List<AppUserVO> getPublisherList(Set idSet) {
        String serviceId = "SERVICE-USER";
        List<ServiceInstance> instanceList = discoveryClient.getInstances(serviceId);
        ServiceInstance userService = instanceList.get(0);
    // 实行动态化调用 地址拼接
        String userServerUrlExecute
                = "http://"+ userService.getHost() + ":" + userService.getPort() + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
//        String userServerUrlExecute
//                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return publisherList;
    }

动态构建eureka集群【eureka】保证高可用

创建一个新的module 其内容和 springcloud-eureka 里面的一样

SwitchHosts配置信息
# imooc-news 192.168.1.3
127.0.0.1 www.imoocnews.com
127.0.0.1 writer.imoocnews.com
127.0.0.1 admin.imoocnews.com
```
127.0.0.1 article.imoocnews.com
127.0.0.1 user.imoocnews.com
127.0.0.1 files.imoocnews.com
127.0.0.1 html.imoocnews.com

# SpringCloud
127.0.0.1 eureka
127.0.0.1 eureka-cluster-7001
127.0.0.1 eureka-cluster-7002
127.0.0.1 eureka-cluster-7003
springcloud-eureka-cluster  application.yml
############################################################
#
# eureka 集群的注册中心
# web访问端口号  约定:7001~7003
#
############################################################
server:
  port: 7001
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-eureka-cluster

############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # 集群中每个eureka的名字都是唯一的
    hostname: eureka-cluster-7001
  client:
    # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

####################################################################################
http://localhost:7001/

DS Replicas
eureka-cluster-7003
eureka-cluster-7002
####################################################################################
如果后面服务很多 100个 那是不是也要创建100个module呢?
并不是 因为每个都是一样的只是改一下application.yml的port端口号而已
所以我们要去把它设置为动态的端口

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

springcloud-eureka-cluster  application.yml【改后】
############################################################
#
# eureka 集群的注册中心
# web访问端口号  约定:7001~7003
#
############################################################
server:
  port: ${7001}
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-eureka-cluster

############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # 集群中每个eureka的名字都是唯一的
    hostname: eureka-cluster-${server.port}
    # 自定义端口号
  other-node-port2: ${p2:7002}
  other-node-port3: ${p3:7003}
  client:
    # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://eureka-cluster-${eureka.other-node-port2}:${eureka.other-node-port2}/eureka/,http://eureka-cluster-${eureka.other-node-port3}:${eureka.other-node-port3}/eureka/

####################################################################################
http://eureka-cluster-7001:7001/  #【可运行】

1一站式管理所有SpringBoot启动类,Services服务窗口 - 喵酱张-Eric - 博客园 (cnblogs.com)
IDEA 2021没有VM options_idea2021怎么找到“vm options”-CSDN博客

复制eureka-cluster-7001服务 变成eureka-cluster-7002 并且在Edit configuration的地方点击Modify options中的Add VM 输入代码:**-DPORT=7002 -DP2=7001 -DP3=7003**
同理弄一个eureka-cluster-7003 输入VM代码:**-DPORT=7003 -DP2=7001 -DP3=7002**
全部启动后都可以在浏览器正常运行 【集群】
http://eureka-cluster-7001:7001/ DS Replicas:eureka-cluster-7003 + eureka-cluster-7002
http://eureka-cluster-7002:7002/ DS Replicas:eureka-cluster-7003 + eureka-cluster-7001
http://eureka-cluster-7003:7003/ DS Replicas:eureka-cluster-7002 + eureka-cluster-7001
如果把里面的application.yml配置注释掉 就可以把自己注册到eureka中
client:
# *是否要把当前的eureka server注册到自己
* register-with-eureka: false
# *从注册中心获得检索服务实例,server没有必要,直接false即可
* fetch-registry: false
之后再重新启动eureka-cluster-7001~7003

eureka-cluster-7001:7001

DS Replicas
Instances currently registered with Eureka
Application AMIs Availability Zones Status
SPRINGCLOUD-EUREKA-CLUSTER n/a (3) (3) UP (3) - localhost:springcloud-eureka-cluster:7003 , localhost:springcloud-eureka-cluster:7001 , localhost:springcloud-eureka-cluster:7002

微服务注册到eureka集群【eureka】${port:8003}

service-user  application.yml
    # 注册中心的服务地址
    service-url:
  # defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/ 三个节点的注册
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
service-article  application.yml
    # 注册中心的服务地址
    service-url:
  # defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

慕课新闻·自媒体接口:8001_api
门户端文章业务的controller → get:/portal/article/detail → articleId:2006117B57WRZGHH

Eureka:7001

DS Replicas
Instances currently registered with Eureka
Application AMIs Availability Zones Status
SERVICE-ARTICLE n/a (1) (1) UP (1) - localhost:service-article:8001
SERVICE-USER n/a (1) (1) UP (1) - localhost:service-user:8003
SPRINGCLOUD-EUREKA-CLUSTER n/a (3) (3) UP (3) - localhost:springcloud-eureka-cluster:7003 , localhost:springcloud-eureka-cluster:7001 , localhost:springcloud-eureka-cluster:7002

构建微服务集集群【eureka】

复制service-user:8003服务 变成service-user:8013 并且在Edit configuration的地方点击Modify options中的Add VM 输入代码:**–DPORT=8013**

service-user  application-dev.yml
server:
  port: ${port:8003}

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
# open mybatis log in dev
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# setup CN from java, This is resource
website:
  domain-name: imoocnews.com

Eureka:7001

DS Replicas
Instances currently registered with Eureka
Application AMIs Availability Zones Status
SERVICE-ARTICLE n/a (1) (1) UP (1) - localhost:service-article:8001
SERVICE-USER n/a (2) (2) UP (2) - localhost:service-user:8003 , localhost:service-user:8013
SPRINGCLOUD-EUREKA-CLUSTER n/a (3) (3) UP (3) - localhost:springcloud-eureka-cluster:7003 , localhost:springcloud-eureka-cluster:7001 , localhost:springcloud-eureka-cluster:7002

实现轮训负载均衡【eureka】

imooc-news-dev-service-user  application-dev.yml
server:
  port: ${port:8003}

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
# open mybatis log in dev
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# setup CN from java, This is resource
website:
  domain-name: imoocnews.com
service-user  com/imooc/user/controller/UserController.java
//每次调用的时候 都会输出其Port
 @Value("${server.port}")
    private String myPort;

    @Override
    public GraceJSONResult queryByIds(String userIds) {
        System.out.println("myPort=" + myPort);
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }
service-article  com/imooc/article/controller/ArticlePortalController.java
    // 注入服务发现,可以获得已经注册的服务相关信息
    @Autowired
    private DiscoveryClient discoveryClient;
    // 发起远程调用,获得用户的基本信息
    private List<AppUserVO> getPublisherList(Set idSet) {
        String serviceId = "SERVICE-USER";
//        List<ServiceInstance> instanceList = discoveryClient.getInstances(serviceId);
//        ServiceInstance userService = instanceList.get(0);
    // 实行动态化调用 地址拼接
        String userServerUrlExecute
                //因为seviceId里面的SERVICE—USER就存在着userService.getHost()和.getPort()
                = "http://"+ serviceId + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
//        String userServerUrlExecute
//                = "http://"+ userService.getHost() + ":" + userService.getPort() + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
//        String userServerUrlExecute
//                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        // 为restTemplate增加一个负载均衡 @CloudConfig 
                // public RestTemplate restTemplate()
        ResponseEntity<GraceJSONResult> responseEntity
                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return publisherList;
    }
service-api  com/imooc/api/config/CloudConfig.java
package com.imooc.api.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CloudConfig {

    public CloudConfig() {
    }
    /**
     * 会基于OKHttp3的配置来实例RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced //添加负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}

慕课新闻·自媒体接口:8001-api
门户端文章业务的controller → articleId:2006117B57WRZGHH

自我保护功能【eureka】

springcloud-eureka-cluster  application.yml
############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # 集群中每个eureka的名字都是唯一的
    hostname: eureka-cluster-${server.port}
    # 自定义端口号
  other-node-port2: ${p2:7002}
  other-node-port3: ${p3:7003}
  client:
    # 是否要把当前的eureka server注册到自己
      # register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
      # fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://eureka-cluster-${eureka.other-node-port2}:${eureka.other-node-port2}/eureka/,http://eureka-cluster-${eureka.other-node-port3}:${eureka.other-node-port3}/eureka/
  server:
    enable-self-preservation: false # 关闭eureka的自我保护功能
    eviction-interval-timer-in-ms: 5000 # 清理无效节点的时间,可以缩短为5s 默认60s
springcloud-eureka application.yml
############################################################
#
# eureka 配置信息
#
############################################################
eureka:
  instance:
    # eureka 实例的hostname,可以是hostname,也可以自定义配置hostname
    hostname: eureka
  client:
    # 是否要把当前的eureka server注册到自己
    register-with-eureka: false
    # 从注册中心获得检索服务实例,server没有必要,直接false即可
    fetch-registry: false
    # 单实例配置自己的服务地址,高可用集群则配置多个地址
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    enable-self-preservation: false # 关闭eureka的自我保护功能
    eviction-interval-timer-in-ms: 5000 # 清理无效节点的时间,可以缩短为5s 默认60s
service-user  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/ 三个节点的注册
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
service-article  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
  instance:
    # 调整微服务(eureka client)和注册中心(eureka server)的心跳时间
    lease-renewal-interval-in-seconds: 3
    # eureka 距离最近的一次心跳等待提出的时间 默认90s
    lease-expiration-duration-in-seconds: 5

Eureka:7001
先把所有服务全启动
eureka:7000
eureka-cluster-7001
eureka-cluster-7002
eureka-cluster-7003
service-article:8001
service-user:8003
service-user:8013
然后去刷新

Instances currently registered with Eureka
Application AMIs Availability Zones Status
SERVICE-ARTICLE n/a (1) (1) UP (1) - localhost:service-article:8001
SERVICE-USER n/a (2) (2) UP (2) - localhost:service-user:8003 , localhost:service-user:8013
SPRINGCLOUD-EUREKA-CLUSTER n/a (3) (3) UP (3) - localhost:springcloud-eureka-cluster:7003 , localhost:springcloud-eureka-cluster:7001 , localhost:springcloud-eureka-cluster:7002

随后只留下eureka-cluster-7001其他全部Stop
再次刷新

Instances currently registered with Eureka
Application AMIs Availability Zones Status
SPRINGCLOUD-EUREKA-CLUSTER n/a (1) (1) UP (1) - localhost:springcloud-eureka-cluster:7001

负载均衡工具

  • Ribbon[本地] = RestTemplate + @LoadBalanced
  • 服务间通信的负载均衡工具,提供完善的超时重试机制

service-api  com/imooc/api/config/CloudConfig.java
package com.imooc.api.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CloudConfig {

    public CloudConfig() {
    }

    /**
     * 会基于OKHttp3的配置来实例RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced //添加负载均衡 默认的负载均衡算法:枚举
    public RestTemplate restTemplate() {

        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}

实现多种负载均衡算法【ribbon】

自定义规则

慕课新闻·自媒体接口api

service-api  com/imooc/api/config/CloudConfig.java
package com.imooc.api.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CloudConfig {

    public CloudConfig() {
    }

    /**
     * 会基于OKHttp3的配置来实例RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced //添加负载均衡 默认的负载均衡算法:枚举
    public RestTemplate restTemplate() {

        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}
service-api  com/rule/MyRule.java
package com.rule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 官方定义了规则不要被 @ComponentScan( 扫描到
@Configuration
public class MyRule {
    @Bean
    public IRule iRule(){// 随机的负载均衡策略
        return new RandomRule();
        // 在调用方article的启动类开启注解 @RibbonClient
    }
}
service-article  com/imooc/article/Application.java
package com.imooc.article;

import com.rule.MyRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
@RibbonClient(name = "service-user", configuration = MyRule.class) //微服务名称
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
运用配置文件进行配置

慕课新闻·自媒体接口api 发送11次请求 门户端文章业务→articleId:2006117B57WRZGHH
service-user:8003请求到myPort=8003 8次
service-user:8013请求到myPort=8013 3次

service-article  resources/application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
  instance:
    # 调整微服务(eureka client)和注册中心(eureka server)的心跳时间
    lease-renewal-interval-in-seconds: 3
    # eureka 距离最近的一次心跳等待提出的时间 默认90s
    lease-expiration-duration-in-seconds: 5

# 配置指定自定义的ribbon规则
SERVICE-USER:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
service-article  com/imooc/article/Application.java
package com.imooc.article;

import com.rule.MyRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
//@RibbonClient(name = "service-user", configuration = MyRule.class) //微服务名称
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

重试机制【ribbon】

节点有可能因为网络问题访问不到 而为了不让其返回错误 需要重试机制
sevice-api  pom.xml
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
service-article  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
  instance:
    # 调整微服务(eureka client)和注册中心(eureka server)的心跳时间
    lease-renewal-interval-in-seconds: 3
    # eureka 距离最近的一次心跳等待提出的时间 默认90s
    lease-expiration-duration-in-seconds: 5

# 配置指定自定义的ribbon规则
SERVICE-USER:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule


ribbon:
  ConnectTimeout: 5000          # 创建连接的超时时间,单位:ms
  ReadTimeout: 5000             # 在连接创建好以后,调用接口的超时时间,单位:ms
  MaxAutoRetries: 1             # 最大重试次数
  MaxAutoRetriesNextServer: 2   # 切换到下个微服务实例的重试次数
  # 当请求到某个微服务5s,超时后会进行重试,先重试连接自己当前的这个实例
  # 如果当前重试失败1次,则会切换到访问集群中的下一个微服务实例,切换最大为2次

logging:
  level:
#    com.imooc.api.controller.user.UserControllerApi: debug
    root: debug

慕课新闻·自媒体接口-8001api articleId:2006117B57WRZGHH
先把所有服务全部启动 然后把service-user:8013 的服务Stop
再去api接口发送请求 查看servcice-article:8001的Console输出日志
14:10.288 [http-nio-8001-exec-1] DEBUG o.s.retry.support.RetryTemplate - Retry: count=0
14:10.774 [http-nio-8001-exec-1] DEBUG o.s.web.client.RestTemplate - Response 200 OK
14:10.288 [http-nio-8001-exec-1] DEBUG o.s.retry.support.RetryTemplate - Retry: count=1
14:10.288 [http-nio-8001-exec-1] DEBUG o.s.retry.support.RetryTemplate - Retry: count=2
14:10.774 [http-nio-8001-exec-1] DEBUG o.s.web.client.RestTemplate - Response 200 OK

简化服务调用【feign】以Api作为接口,面向接口的编程风格

声明式HTTP工具
  • Feign
  • 声明式的http工具,用于简化服务调用
service-api  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
service-article  com/imooc/article/controller/ArticlePortalController.java
    // 注入服务发现,可以获得已经注册的服务相关信息
    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private UserControllerApi userControllerApi;
    //面向接口 UserControllerApi
    // 发起远程调用,获得用户的基本信息
    private List<AppUserVO> getPublisherList(Set idSet) {
//        String serviceId = "SERVICE-USER";
//        List<ServiceInstance> instanceList = discoveryClient.getInstances(serviceId);
//        ServiceInstance userService = instanceList.get(0);
    // 实行动态化调用 地址拼接
//        String userServerUrlExecute
                //因为seviceId里面的SERVICE—USER就存在着userService.getHost()和.getPort()
//                = "http://"+ serviceId + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        GraceJSONResult bodyResult = userControllerApi.queryByIds(JsonUtils.objectToJson(idSet));
//        String userServerUrlExecute
//                = "http://"+ userService.getHost() + ":" + userService.getPort() + "/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
//        String userServerUrlExecute
//                = "http://user.imoocnews.com:8003/user/queryByIds?userIds=" + JsonUtils.objectToJson(idSet);
        // 为restTemplate增加一个负载均衡@CloudConfig public RestTemplate restTemplate()
//        ResponseEntity<GraceJSONResult> responseEntity
//                = restTemplate.getForEntity(userServerUrlExecute, GraceJSONResult.class);
//        GraceJSONResult bodyResult = responseEntity.getBody();
        List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }
        return publisherList;
    }
service-article  com/imooc/article/Application.java //【EnableFeignClients】
package com.imooc.article;

import com.rule.MyRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
//@RibbonClient(name = "service-user", configuration = MyRule.class) //微服务名称
@EnableFeignClients({"com.imooc"})
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
service-api  com/imooc/api/config/MyServiceList.java
package com.imooc.api.config;

public class MyServiceList {
    public static final String SERVICE_USER = "service-user";
}
service-article  com/imooc/api/controller/user/UserControllerApi.java【@FeignClient】
package com.imooc.api.controller.user;

import com.imooc.api.config.MyServiceList;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user")
@FeignClient(value = MyServiceList.SERVICE_USER) //作为客户端直接调用
public interface UserControllerApi {

    @ApiOperation(value = "获得用户基本信息",notes = "获得用户基本信息",httpMethod = "POST")
    @PostMapping("/getUserInfo")
    public GraceJSONResult getUserInfo(@RequestParam String userId);
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
                                          @RequestParam BindingResult result); //RequestParam  BindingResult result 加了肯定在前端不可用 对Feign而言不能存在两个对象不然会认为有两个model

    @ApiOperation(value = "根据用户的ids查询用户列表",notes = "根据用户的ids查询用户列表",httpMethod = "GET")
    @GetMapping("/queryByIds")
    public GraceJSONResult queryByIds(@RequestParam String userIds);
}

统一检验处理【feign】

把所有的BindingResult都可以采用全局调用的方法去调用
慕课新闻·自媒体接口8003api → 用户信息相关 → 修改/完善用户信息 Post /user/updateUserInfo

dev-common  com/imooc/exception/GraceExceptionHandler.java
package com.imooc.exception;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import javax.naming.Binding;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 统一异常拦截处理
 * 可以针对异常的类型进行捕获 然后返回json信息到前端
 */
@ControllerAdvice
public class GraceExceptionHandler {
 /* @ExceptionHandler(MyCustomException.class)
    //只要是这个类的异常都会进入下面的方法
    @ResponseBody
    public GraceJSONResult returnMyException(MyCustomException e){
        e.printStackTrace(); //打印信息
        return GraceJSONResult.exception(e.getResponseStatusEnum());
    }

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    @ResponseBody
    public GraceJSONResult returnMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
        return GraceJSONResult.errorCustom(ResponseStatusEnum.FILE_MAX_SIZE_ERROR);
    }*/

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody //该异常是基于所有的vo验证
    public GraceJSONResult returnException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        Map<String, String> map = getErrors(result);
        return GraceJSONResult.errorMap(map);
    }

    public Map<String, String> getErrors(BindingResult result) {
        Map<String, String> map = new HashMap<>();
        List<FieldError> errorList = result.getFieldErrors();
        for (FieldError error : errorList) {
            // 发送验证错误的时候所对应的某个属性
            String field = error.getField();
            // 验证的错误消息
            String msg = error.getDefaultMessage();
            map.put(field, msg);
        }
        return map;
    }
}
service-api  com/imooc/api/controller/user/UserControllerApi.java
//    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
//    @PostMapping("/updateUserInfo")
//    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
//                                          @RequestParam BindingResult result);
    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO);
service-user  com/imooc/user/controller/UserController.java
    @Override
  public GraceJSONResult updateUserInfo(@Valid UpdateUserInfoBO updateUserInfoBO){
    //, BindingResult result) {
//        // 0.校验BO
//        if (result.hasErrors()){
//            Map<String, String> map = getErrors(result);
//            return GraceJSONResult.errorMap(map);
//        }
        // 1.执行更新操作
        userService.updateUserInfo(updateUserInfoBO);
        return GraceJSONResult.ok();
        //调用UserService把独有信息传入
    }

开启日志调式【feign】基于http的调用

service-article  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  # 自定义eureka server的信息
  server:
    hostname: eureka
    port: 7000
  client:
    # 所有的微服务都必须注册到eureka中
    register-with-eureka: true
    # 从注册中心获得检索服务实例
    fetch-registry: true

    # 注册中心的服务地址
    service-url:
#     defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
  instance:
    # 调整微服务(eureka client)和注册中心(eureka server)的心跳时间
    lease-renewal-interval-in-seconds: 3
    # eureka 距离最近的一次心跳等待提出的时间 默认90s
    lease-expiration-duration-in-seconds: 5

# 配置指定自定义的ribbon规则
SERVICE-USER:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule


ribbon:
  ConnectTimeout: 5000          # 创建连接的超时时间,单位:ms
  ReadTimeout: 5000             # 在连接创建好以后,调用接口的超时时间,单位:ms
  MaxAutoRetries: 1             # 最大重试次数
  MaxAutoRetriesNextServer: 2   # 切换到下个微服务实例的重试次数
  # 当请求到某个微服务5s,超时后会进行重试,先重试连接自己当前的这个实例
  # 如果当前重试失败1次,则会切换到访问集群中的下一个微服务实例,切换最大为2次

logging:
  level:
    com.imooc.api.controller.user.UserControllerApi: debug
#    root: debug 日志打印级别

# 配置feign
feign:
  client:
    config:
      # 配置服务提供方的名称
      service-user:
        logger-level: full

重启所有服务 调用慕课新闻·自媒体接口8001api门户端→文章详情 articleId:2006117B57WRZGHH
09:19.009 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] <— HTTP/1.1 200 (590ms)
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] connection: keep-alive
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] content-type: application/json
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] date: Mon, 05 Aug 2024 09:09:19 GMT
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] keep-alive: timeout=60
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] transfer-encoding: chunked
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] vary: Access-Control-Request-Headers
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] vary: Access-Control-Request-Method
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] vary: Origin
09:19.011 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds]
09:19.013 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] {“status”:200,”msg”:”操作成功!”,”success”:true,”data”:[{“id”:”200628AFYM7AGWPH”,”nickname”:”我是慕课网”,”face”:”https://imooc-news-dev.oss-cn-shanghai.aliyuncs.com/images/abc/200628AFYM7AGWPH/2007088XH2WT7GXP.png","activeStatus":1,"myFollowCounts":null,"myFansCounts":null}]}
09:19.013 [http-nio-8001-exec-7] DEBUG c.i.a.c.user.UserControllerApi - [UserControllerApi#queryByIds] <— END HTTP (286-byte body)
09:19.518 [PollingServerListUpdater-0] INFO c.n.config.ChainedDynamicProperty - Flipping property: service-user.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
09:21.013 [scheduling-1] INFO c.imooc.api.aspect.ServiceLogAspect - 开始执行 class com.imooc.article.service.impl.ArticleServiceImpl.updateAppointToPublish

阐述断路器及概念【hystrix】

断路器
  • Hystrix
  • 提供容错机制,避免微服务系统雪崩
服务熔断与降级

模拟服务故障【hystrix】

慕课新闻·自媒体接口8001api 同上
会报Timeout超时的异常

service-user  com/imooc/user/controller/UserController.java
   @Value("${server.port}")
    private String myPort;

    @Override
    public GraceJSONResult queryByIds(String userIds) {
        // 1.手动触发异常
        int a = 1/0;
        // 2.模拟超时异常
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


        System.out.println("myPort=" + myPort);
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }
service-api  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

服务提供者熔断【hystrix】

service-user  com/imooc/user/controller/UserController.java
@Value("${server.port}")
    private String myPort;
    // 添加熔断机制 一旦熔断会有替补方法[降级的方法]
    @HystrixCommand(fallbackMethod = "queryByIdsFallback")
    @Override
    public GraceJSONResult queryByIds(String userIds) {
        // 1.手动触发异常
        int a = 1/0;
        // 2.模拟超时异常
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("myPort=" + myPort);
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }


    public GraceJSONResult queryByIdsFallback(String userIds) {
        System.out.println("进入降级方法:queryByIdsFallback");

        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        for (String userId : userIdList){
            // 手动构建空对象,详情页所展示的用户信息可有可无 返回空对象
            AppUserVO userVO = new AppUserVO();
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }
service-user  com/imooc/user/Application.java
package com.imooc.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@MapperScan(basePackages = "com.imooc.user.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient   // 开启eureka client 注册到server中
@EnableCircuitBreaker // 开启hystrix的熔断机制
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
===============================================================
进入降级方法
service-user  application.yml
#  配置hystrix
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000   # 设置hystrix超时时间,超过2秒触发降级

全局降级【hystrix】

只需要return一个错误就行了没必要100个方法写100个降级
{

​ status: 555,
​ msg: “”全局降级:系统繁忙,请稍后再试!””
​ success: false,
​ data: null

}

service-user  com/imooc/user/controller/UserController.java
@RestController
@DefaultProperties(defaultFallback = "defaultFallback")
public class UserController extends BaseController implements UserControllerApi {
    final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    // 其他方法一旦发现异常就会进入这个方法里面 全局唯一 其他的降级方法要注释
    public GraceJSONResult defaultFallback(){
        return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_ERROR_GLOBAL);
    }
......
}
// 改动是为了不报错空指针异常 因为已经变成了全局降级  降级的错误信息要调整
    @Autowired
    private UserControllerApi userControllerApi;
    //面向接口 UserControllerApi
    // 发起远程调用,获得用户的基本信息
   /* private List<AppUserVO> getPublisherList(Set idSet) {
GraceJSONResult bodyResult = userControllerApi.queryByIds(JsonUtils.objectToJson(idSet)); 
List<AppUserVO> publisherList = null;
        if (bodyResult.getStatus() == 200) {
            String userJson = JsonUtils.objectToJson(bodyResult.getData());
            publisherList = JsonUtils.jsonToList(userJson, AppUserVO.class);
        }*/ else {
            publisherList = new ArrayList<>();
        }
        return publisherList;
    }
dev-common  com/imooc/grace/result/ResponseStatusEnum.java
 // 系统错误,未预期的错误 555
    SYSTEM_ERROR(555, false, "系统繁忙,请稍后再试!"),
    SYSTEM_OPERATION_ERROR(556, false, "操作失败,请重试或联系管理员"),
    SYSTEM_RESPONSE_NO_INFO(557, false, ""),
    SYSTEM_ERROR_GLOBAL(558, false, "全局降级:系统繁忙,请稍后再试!"),
    SYSTEM_ERROR_FEIGN(559, false, "客户端Feign降级:系统繁忙,请稍后再试!"),
    SYSTEM_ERROR_ZUUL(560, false, "请求系统过于繁忙,请稍后再试!");

服务调用者降级【hystrix】

service-article  application.yml
# 配置feign
feign:
  client:
    config:
      # 配置服务提供方的名称
      service-user:
        logger-level: full

  hystrix:  #打开feign客户端的内置hystrix
    enabled: true
service-article  com/imooc/article/Application.java //【增加一个@EnableHystrix】
package com.imooc.article;

import com.rule.MyRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@EnableSwagger2
@SpringBootApplication
@MapperScan(basePackages = "com.imooc.article.mapper")
@ComponentScan(basePackages = {"com.imooc","org.n3r.idworker"})
@EnableEurekaClient
//@RibbonClient(name = "service-user", configuration = MyRule.class) //微服务名称
@EnableFeignClients({"com.imooc"})
@EnableHystrix
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
====================================================================================
service-article:8001  进入客户端(服务调用者)的降级方法
service-api  com/imooc/api/controller/user/UserControllerApi.java//【@FeignClient增加fallbackFactory】
package com.imooc.api.controller.user;

import com.imooc.api.config.MyServiceList;
import com.imooc.api.controller.user.fallbacks.UserControllerFactoryFallback;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.pojo.bo.RegistLoginBO;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

@Api(value = "用户信息相关Controller",tags = {"用户信息相关Controller"})
@RequestMapping("user") //fallbackFactory所有方法的降级
@FeignClient(value = MyServiceList.SERVICE_USER, fallbackFactory = UserControllerFactoryFallback.class ) //作为客户端直接调用
public interface UserControllerApi {

    @ApiOperation(value = "获得用户基本信息",notes = "获得用户基本信息",httpMethod = "POST")
    @PostMapping("/getUserInfo")
    public GraceJSONResult getUserInfo(@RequestParam String userId);
    @ApiOperation(value = "获得用户账户信息",notes = "获得用户账户信息",httpMethod = "POST")
    @PostMapping("/getAccountInfo")
    public GraceJSONResult getAccountInfo(@RequestParam String userId);

//    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
//    @PostMapping("/updateUserInfo")
//    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO,
//                                          @RequestParam BindingResult result);
    @ApiOperation(value = "修改/完善用户信息",notes = "修改/完善用户信息",httpMethod = "POST")
    @PostMapping("/updateUserInfo")
    public GraceJSONResult updateUserInfo(@RequestBody @Valid UpdateUserInfoBO updateUserInfoBO);


    @ApiOperation(value = "根据用户的ids查询用户列表",notes = "根据用户的ids查询用户列表",httpMethod = "GET")
    @GetMapping("/queryByIds")
    public GraceJSONResult queryByIds(@RequestParam String userIds);
}
service-api  com/imooc/api/controller/user/fallbacks/UserControllerFactoryFallback.java
package com.imooc.api.controller.user.fallbacks;

import com.imooc.api.controller.user.UserControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.bo.UpdateUserInfoBO;
import com.imooc.pojo.vo.AppUserVO;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;

@Component //这个类让容器加载
public class UserControllerFactoryFallback implements FallbackFactory<UserControllerApi> {
    @Override
    public UserControllerApi create(Throwable throwable) {
 // 重写的过程就是降级的过程
        return new UserControllerApi() {
            //SYSTEM_ERROR_FEIGN(559, false, "客户端Feign降级:系统繁忙,请稍后再试!")
            @Override
            public GraceJSONResult getUserInfo(String userId) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_ERROR_FEIGN);
            }

            @Override
            public GraceJSONResult getAccountInfo(String userId) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_ERROR_FEIGN);
            }

            @Override
            public GraceJSONResult updateUserInfo(@Valid UpdateUserInfoBO updateUserInfoBO) {
                return GraceJSONResult.errorCustom(ResponseStatusEnum.SYSTEM_ERROR_FEIGN);
            }

            @Override
            public GraceJSONResult queryByIds(String userIds) {
                System.out.println("进入客户端(服务调用者)的降级方法");
                List<AppUserVO> publisherList = new ArrayList<>();
                return GraceJSONResult.ok(publisherList);
            }
        };
    }
}

自动触发熔断隔离与恢复【hystrix】

service-user  application.yml #【配置熔断器】
#  配置hystrix
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000   # 设置hystrix超时时间,超过2秒触发降级
      circuitBreaker:   # 配置断路器
        enabled: true
        requestVolumeThreshold: 10    # 触发熔断最小请求次数,默认:20
        sleepWindowInMilliseconds: 15000    # 熔断后过几秒后尝试半开状态(请求重试),默认:5s
        errorThresholdPercentage: 50  # 触发熔断的失败率(异常率/阈值),默认:50
service-user  com/imooc/user/controller/UserController.java //[FIXME:]
    @Value("${server.port}")
    private String myPort;
    // 添加熔断机制 一旦熔断会有替补方法[降级的方法]
    @HystrixCommand//(fallbackMethod = "queryByIdsFallback")
    @Override
    public GraceJSONResult queryByIds(String userIds) {
        // 1.手动触发异常
        int a = 1/0;
        // 2.模拟超时异常
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }


        System.out.println("myPort=" + myPort);
        if (StringUtils.isBlank(userIds)){
            return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_NOT_EXIST_ERROR);
        }
        List<AppUserVO> publisherList = new ArrayList<>();
        List<String> userIdList = JsonUtils.jsonToList(userIds, String.class);//传过来两个用户的id
        // FIXME: 仅用于dev测试,硬编码动态判断来抛出异常
        if (userIdList.size() > 1){
            System.out.println("出现异常~~");
            throw new RuntimeException("出现异常~~");
        }

        for (String userId : userIdList){
            //获得用户基本信息
            AppUserVO userVO = getBasicUserInfo(userId);
            // 3.添加到publisherList
            publisherList.add(userVO);
        }
        return GraceJSONResult.ok(publisherList);
    }

微服务网关【zuul】维护微服务的ip地址

服务网关
  • Zuul (祖尔)
  • 微服务的网关,可以实现动态路由、过滤器等功能

构建网关微服务【zuul】

springcloud-zuul-server  com/imooc/zuul/Application.java
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
                                    MongoAutoConfiguration.class})
@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
//@EnableZuulServer
@EnableZuulProxy      // @EnableZuulProxy是@EnableZuulServer的一个增强升级版,当zuul和eureka、ribbon等组件共同使用,则使用增强版即可
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
springcloud-zuul-server  logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/springcloud-zuul"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/zuul.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
service-api  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
springcloud-zuul-server  pom.xml 【exclusions是重点】
<?xml version="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>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-zuul-server</artifactId>

    <dependencies>

        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version> <!--排除包-->
                <exclusions>
                    <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                    </exclusion>
                </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

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

        <!--<dependency>-->
        <!--<groupId>org.springframework.cloud</groupId>-->
        <!--<artifactId>spring-cloud-starter-bus-amqp</artifactId>-->
        <!--</dependency>-->

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

    </dependencies>

</project>
springcloud-zuul-server  application.yml 
############################################################
#
# 网关zuul
# web访问端口号  约定:7070
#
############################################################
server:
  port: 7070
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-zuul-server

  redis:
    database: 0
    host: 192.168.1.201
    port: 6379
    password: 123456
  zipkin:
    # 配置zipkin采集的服务地址,数据会发送到这里
    base-url: http://192.168.1.2:9411/
    sender:
      # 数据采集的传输通信方式,web http的形式
      type: web
  sleuth:
    sampler:
      # 数据采样比例(百分数),0~1
      probability: 1
zuul-server  com/imooc/zuul/controller/HelloController.java
package com.imooc.zuul.controller;

import com.imooc.grace.result.GraceJSONResult;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/hello")
    public Object hello() {
        return GraceJSONResult.ok();
    }
}

配置路由【zuul】

zuul-server  application.yml
# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
      path: /service-article/**       # 请求路径(前缀)
      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
localhost:8001/portal/article/detail?articleId=2006117B57WRZGHH
直接可以访问到详情数据
微服务网关→ 7070
// service-article: /service-article/** # 请求路径(前缀**)
# localhost:7070/service-article/portal/detail?articleId=2006117B57WRZGHH

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
      path: /service-article/**       # 请求路径(前缀)
      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀
# localhost:7070/api/service-article/portal/detail?articleId=2006117B57WRZGHH

配置微服务实例的路由【zuul】

在路由规则里面的  url: http://192.168.1.2:8001  很容易发生变化
所以直接去请求eureka的SERVICE-USER/ARTICLE
springcloud-zuul-server  pom.xml 【取消exclusions注释 包含eureka client】
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version> <!--排除包-->
          </dependency>
zuul-server  application.yml
############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
springcloud-zuul-server  com/imooc/zuul/Application.java 【打开@EnableEurekaClient】
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
                                    MongoAutoConfiguration.class})
@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
@EnableEurekaClient
//@EnableZuulServer
@EnableZuulProxy      // @EnableZuulProxy是@EnableZuulServer的一个增强升级版,当zuul和eureka、ribbon等组件共同使用,则使用增强版即可
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
zuul-server  application.yml 【实现service-id进行请求转发 ip发生变化没有关系】
# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
      service-article:                  # 配置微服务的路由id,微服务的实例id
      path: /service-article/**       # 请求路径(前缀)
      service-id: service-article     # 请求转发的微服务实例id
#     url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 简化版本 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀

过滤器【zuul】网端ip黑名单拦截

zuul-server  com/imooc/zuul/filters/MyFilter.java
package com.imooc.zuul.filters;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

/**
 * 构建zuul的自定义过滤器
 */
@Component
public class MyFilter extends ZuulFilter {

    /**
     * 定义过滤器的类型
     *      pre:    在请求被路由之前执行
     *      route:  在路由请求的时候执行
     *      post:   请求路由以后执行
     *      error:  处理请求时发生错误的时候执行
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器执行的顺序,配置多个有顺序的过滤
     * 执行顺序从小到大
     * @return
     */
    @Override
    public int filterOrder() {
        return 1;
    }

    /**
     * 是否开启过滤器
     *      true:开启
     *      false:禁用
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器的业务实现
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {

        System.out.println("display pre zuul filter...");

        return null;    // 没有意义可以不用管。
    }
}

localhost:7070/api/service-article/portal/detail?articleId=2006117B57WRZGHH
刷新成功后 在zuul-7070服务console会有一行
display pre zuul filter… [启动成功]

限制ip黑名单的频繁请求【zuul】

zuul-server  application.yml 【增加ip请求限制的参数配置】
# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
      service-article:                  # 配置微服务的路由id,微服务的实例id
      path: /service-article/**       # 请求路径(前缀)
      service-id: service-article     # 请求转发的微服务实例id
#     url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 简化版本 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀

# ip请求限制的参数配置
blackIp:
  continueCounts: ${counts:10}    # ip连续请求的次数
  timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
  limitTimes: ${times:15}         # 限制的事件,单位:秒
zuul-server  com/imooc/zuul/filters/BlackIPFilter.java
package com.imooc.zuul.filters;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.RedisOperator;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
/* application.yml
*  blackIp:
   continueCounts: ${counts:10}    # ip连续请求的次数
   timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
   limitTimes: ${times:15}         # 限制的事件,单位:秒
* */
@Component
@RefreshScope
public class BlackIPFilter extends ZuulFilter {

    @Value("${blackIp.continueCounts}")
    public Integer continueCounts;
    @Value("${blackIp.timeInterval}")
    public Integer timeInterval;
    @Value("${blackIp.limitTimes}")
    public Integer limitTimes;

    @Autowired
    private RedisOperator redis;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {

        System.out.println("执行【ip黑名单】过滤器...");

        System.out.println("continueCounts: " + continueCounts);
        System.out.println("timeInterval: " + timeInterval);
        System.out.println("limitTimes: " + limitTimes);


        // 获得上下文对象
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();

        // 获得ip
        String ip = IPUtil.getRequestIp(request);

        /**
         * 需求:
         *  判断ip在10秒内的请求次数是否超过10次
         *  如果超过,则限制这个ip访问15秒,15秒以后再放行
         */

        final String ipRedisKey = "zuul-ip:" + ip;
        final String ipRedisLimitKey = "zuul-ip-limit:" + ip;

        // 获得当前ip这个key的剩余时间
        long limitLeftTime = redis.ttl(ipRedisLimitKey);
        // 如果当前限制ip的key还存在剩余时间,说明这个ip不能访问,继续等待
        if (limitLeftTime > 0) {
            stopRequest(context);
            return null;
        }

        // 在redis中累加ip的请求访问次数
        long requestCounts = redis.increment(ipRedisKey, 1);
        // 从0开始计算请求次数,初期访问为1,则设置过期时间,也就是连续请求的间隔时间
        if (requestCounts == 1) {
            redis.expire(ipRedisKey, timeInterval);
        }

        // 如果还能取得请求次数,说明用户连续请求的次数落在10秒内
        // 一旦请求次数超过了连续访问的次数,则需要限制这个ip的访问
        if (requestCounts > continueCounts) {
            // 限制ip的访问时间
            redis.set(ipRedisLimitKey, ipRedisLimitKey, limitTimes);
            stopRequest(context);
        }

        return null;
    }

    private void stopRequest(RequestContext context) {
        // 停止zuul继续向下路由,禁止请求通信
        context.setSendZuulResponse(false);
        context.setResponseStatusCode(200);
        String result = JsonUtils.objectToJson(
                GraceJSONResult.errorCustom(
                        ResponseStatusEnum.SYSTEM_ERROR_ZUUL));
        context.setResponseBody(result);
        context.getResponse().setCharacterEncoding("utf-8");
        context.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE);
    }
}

=============================================================================
dev-common  com/imooc/grace/result/ResponseStatusEnum.java
SYSTEM_ERROR_ZUUL(560, false, "请求系统过于繁忙,请稍后再试!");
zuul-server  application.yml 【把redis增加进来】
############################################################
#
# 网关zuul
# web访问端口号  约定:7070
#
############################################################
server:
  port: 7070
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-zuul-server

  redis:
    database: 0
    host: 192.168.1.201
    port: 6379
    password: 123456
  zipkin:
    # 配置zipkin采集的服务地址,数据会发送到这里
    base-url: http://192.168.1.2:9411/
    sender:
      # 数据采集的传输通信方式,web http的形式
      type: web
  sleuth:
    sampler:
      # 数据采样比例(百分数),0~1
      probability: 1

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀



# ip请求限制的参数配置
blackIp:
  continueCounts: ${counts:10}    # ip连续请求的次数
  timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
  limitTimes: ${times:15}         # 限制的事件,单位:秒

此时redis中会出现
zuul-ip(1) → zuul-ip → Value:1
zuul-ip-limit(1) → zuul-ip-limit: 本机地址
在zuul-7070服务里的Console 显示:
display pre zuul filter …
执行【ip黑名单】过滤器…

分布式配置中心【config】

  • SpringCloud Config
  • 为所有服务提供统一的配置管理服务微服务配置一下子全部生效
  • 包含配置服务端与配置客户端
配置中心的功能
  • 统一管理配置
  • 管理不同环境下的配置
  • 动态调整配置

搭配配置中心【config】

leechenxiang/imooc-news-config (github.com)

zuul-dev.yml
blackIp:
  continueCounts: 10
  timeInterval: 10
  limitTimes: 15

zuul-prod.yml
blackIp:
  continueCounts: 40
  timeInterval: 70
  limitTimes: 315
springcloud-config  pom.xml
<?xml version="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>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-config</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
    </dependencies>
</project>
springcloud-config  com/imooc/config/Application.java
package com.imooc.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
                                    MongoAutoConfiguration.class})
@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
@EnableEurekaClient
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
==========================================================================
config-7080
springcloud-config  application.yml
############################################################
#
# 配置服务Config
# web访问端口号  约定:7080
#
############################################################
server:
  port: 7080
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-config
  
  rabbitmq:
    host: 192.168.1.204
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/


# 配置动态刷新git配置的路径终端请求地址
management:
  endpoints:
    web:
      exposure:
        include: "*"
springcloud-config  logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <!-- 指定日志文件的存储地址,使用绝对路径 -->
    <property name="LOG_HOME" value="/workspaces/logs/imooc-news-dev/springcloud-config"/>

    <!-- Console 输出设置 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%white(%d{mm:ss.SSS}) %green([%thread]) %cyan(%-5level) %yellow(%logger{36}) %magenta(-) %black(%msg%n)</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <fileNamePattern>${LOG_HOME}/config.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">-->
        <!--<appender-ref ref="CONSOLE"/>-->
    <!--</logger>-->

    <root level="info">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
springcloud-config  com/imooc/config/controller/HelloController.java
package com.imooc.config.controller;

import com.imooc.grace.result.GraceJSONResult;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public Object hello() {
        return GraceJSONResult.ok();
    }
}

配置中心实现git配置读取【config】

springcloud-config  pom.xml
<?xml version="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>imooc-news-dev</artifactId>
        <groupId>com.imooc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-config</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>imooc-news-dev-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
    </dependencies>

</project>
springcloud-config  application.yml
#增加  cloud:config:server:git: uri
############################################################
#
# 配置服务Config
# web访问端口号  约定:7080
#
############################################################
server:
  port: 7080
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-config
  cloud:
    config:
      server:
        git:
          uri: https://github.com/leechenxiang/imooc-news-config.git
  rabbitmq:
    host: 192.168.1.204
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/
springcloud-config  com/imooc/config/Application.java
package com.imooc.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
                                    MongoAutoConfiguration.class})
@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
@EnableEurekaClient
@EnableConfigServer //开启这个配置中心
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

leechenxiang/imooc-news-config (github.com)

localhost:7080/zuul-prod.yml 这里直接引用了github上面的yml
localhost:7080/master/zuul-prod.yml

zuul-dev.yml
blackIp:
  continueCounts: 10
  timeInterval: 10
  limitTimes: 15

zuul-prod.yml
blackIp:
  continueCounts: 40
  timeInterval: 70
  limitTimes: 315

配置客户端拉取配置

zuul-server  pom.xml
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
zuul-server  resources/bootstrap.yml
############################################################
# 系统全局加载文件 先加载这个文件
# 网关zuul
# web访问端口号  约定:7070
#
############################################################
server:
  port: 7070
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-zuul-server
  redis:
    database: 0
    host: 192.168.1.201
    port: 6379
    password: 123456
  cloud:
    config:
      label: master # github上的分支
      name: zuul # 定义的服务
      profile: prod  # 所加载的环境变量
#      uri: http://192.168.1.2:7080
      discovery:
        enabled: true
        service-id: springcloud-config


############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/**
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址
  prefix: /api                        # 请求前缀
启动zuul-7070  //客户端连接config(server)并且动态获得了github的数据
打印控制台Console
display pre zuul filter...
执行【ip黑名单】过滤器...
// zuul-prod.yml
  continueCounts: 40
  timeInterval: 70
  limitTimes: 315

动态刷新git配置

// zuul-prod.yml 动态修改数值
  continueCounts: 35
  timeInterval: 305
  limitTimes: 65
在浏览器会显示更新后的数值 但是在console打印台不会显示改后的 只会显示之前的
需要重启服务才可以达到修改后的效果 显示修改的数值
zuul-server  pom.xml
添加健康检测的配置
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
zuul-server  com/imooc/zuul/filters/BlackIPFilter.java
//【开启刷新 @RefreshScope 不能全自动 需要触碰某些请求 在yaml配置动态刷新的地址】
package com.imooc.zuul.filters;

import com.imooc.grace.result.GraceJSONResult;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.RedisOperator;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
/* application.yml
*  blackIp:
   continueCounts: ${counts:10}    # ip连续请求的次数
   timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
   limitTimes: ${times:15}         # 限制的事件,单位:秒
* */
@Component
@RefreshScope
public class BlackIPFilter extends ZuulFilter {

    @Value("${blackIp.continueCounts}")
    public Integer continueCounts;
    @Value("${blackIp.timeInterval}")
    public Integer timeInterval;
    @Value("${blackIp.limitTimes}")
    public Integer limitTimes;

    @Autowired
    private RedisOperator redis;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }
}
zuul-server  bootstrap.yml  +  springcloud-config  application.yaml
# 配置动态刷新git配置的路径终端请求地址 只需要通过URL请求不用重启就可以自动刷新 后期要通过脚本运行才可【通过请求处理 postman → POST → https://localhost:7070/actuator/refresh】
management:
  endpoints:
    web:
      exposure:
        include: refresh
# 此时回到console就可以有更新后的数据显示了

消息总线概述【bus】[RabbitMQ]

Config遗留问题
  • 手动刷新与业务耦合 [也可能在文章/user模块发生]
  • Config配置中心的刷新去解决动态刷新
  • N个微服务端需要N次手动书信
消息总线(要和config与微服务端进行配置)
  • SpringCloud Bus
  • 为SpringCloud Config提供增益buff
  • 可以实现配置自动刷新[1k个 1w个]

配置实现消息统一发送【bus】

config把消息推給zull-server

springcloud-config  pom.xml    +    zuul-server  pom.xml
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
zuul-server  resources/bootstrap.yaml  +  springcloud-config  application.yaml
  rabbitmq:
    host: 192.168.1.204
    port: 5672
    username: admin
    password: admin
    virtual-host: imooc-news-dev

实现刷新server端达到所有的刷新配置
【通过请求处理 postman → POST → https://localhost:7080/actuator/bus-refresh】
若想精确刷新某个服务 需要拼接
postman → POST → https://localhost:7080/actuator/bus-refresh/{微服务的实例id}:{port}
微服务实例id是yaml配置项目信息 → spring.application.name:springcloud-zuul-server
https://localhost:7080/actuator/bus-refresh/springcloud-zuul-server:7070 【目标微服务地址实现精确打击】

消息驱动概述【stream】

消息驱动
  • SpringCloud Stream
  • 统一封装消息的服务框架
  • RabbitMQ,RocketMQ,Kafka,ActiveMQ,ZeroMQ,…
Stream消息模型

实现消费者与生产者【stream】

将zuul-server的resources/application.yml 恢复到原来配置

############################################################
#
# 网关zuul
# web访问端口号  约定:7070
#
############################################################
server:
  port: 7070
  tomcat:
    uri-encoding: UTF-8

############################################################
#
# 配置项目信息
#
############################################################
spring:
  application:
    name: springcloud-zuul-server

  redis:
    database: 0
    host: 192.168.1.201
    port: 6379
    password: 123456
  zipkin:
    # 配置zipkin采集的服务地址,数据会发送到这里
    base-url: http://192.168.1.2:9411/
    sender:
      # 数据采集的传输通信方式,web http的形式
      type: web
  sleuth:
    sampler:
      # 数据采样比例(百分数),0~1
      probability: 1

############################################################
#
# eureka client 配置信息
#
############################################################
eureka:
  server:
    hostname: eureka
    port: 7000
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      #defaultZone: http://${eureka.server.hostname}:${eureka.server.port}/eureka/
      defaultZone: http://eureka-cluster-7001:7001/eureka/,http://eureka-cluster-7002:7002/eureka/,http://eureka-cluster-7003:7003/eureka/

# 路由规则: http://[网关地址]:[端口号]/[prefix]/[微服务实例id]/[请求地址路径]
zuul:
  routes:
    # 由于路由id和微服务实例id相同,我们可以简化转发的配置
    service-article: /service-article/** # 请求路径(前缀)
  #    service-article:                  # 配置微服务的路由id,微服务的实例id
#      path: /service-article/**       # 请求路径(前缀)
#      service-id: service-article     # 请求转发的微服务实例id
#      url: http://192.168.1.2:8001    # 请求转发到指定的微服务所在的ip地址(8001文章服务)
  prefix: /api                        # 请求前缀



# ip请求限制的参数配置
blackIp:
  continueCounts: ${counts:10}    # ip连续请求的次数
  timeInterval: ${interval:10}    # ip判断的事件间隔,单位:秒
  limitTimes: ${times:15}         # 限制的事件,单位:秒
service-article  pom.xml  +  service-user  pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
            <version>4.1.1</version>
        </dependency>
service-article  application.yaml      +      service-user  application.yaml
    cloud:
    stream:
      bindings:                    # 绑定通道和交换机
        myOutput:                   # 定义生产者的通道
          # 自定义交换机的名字,也就是代码里构建的信息,交给底层mq的交换机
          destination:
        myInput:                    # 定义消费者通道
          # 自定义交换机的名字,也就是消息从底层mq输入到消费端进行消费
          destination:
service-article  com/imooc/article/stream/MyStreamChannel.java
package com.imooc.article.stream;

import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.stereotype.Component;

/**
 * 声明构建通道channel
 */
@Component
public interface MyStreamChannel {

    String OUTPUT = "myOutput";
    String INPUT = "myInput";

    @Output(MyStreamChannel.OUTPUT)
    MessageChannel output();

    @Input(MyStreamChannel.INPUT)
    SubscribableChannel input();  //订阅能力的通道
}
service-article  com/imooc/article/stream/MyStreamChannel.java
package com.imooc.article.stream;


import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.stereotype.Component;

/**
 * 声明构建通道channel
 */
@Component
public interface MyStreamChannel {
    String OUTPUT = "myOutput";
    String INPUT = "myInput";

    @Output(MyStreamChannel.OUTPUT)
    MessageChannel output();

    @Input(MyStreamChannel.INPUT)
    SubscribableChannel input();
}
service-article  com/imooc/article/stream/StreamService.java
package com.imooc.article.stream;

public interface StreamService {
    public void sendMsg();
    public void eat(String dumpling);
}
service-article  com/imooc/article/stream/StreamServiceImpl.java
package com.imooc.article.stream;

import com.imooc.pojo.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

/**
 * 开启绑定器
 * 绑定通道channel
 */
@Component
@EnableBinding(MyStreamChannel.class)
public class StreamServiceImpl implements StreamService {

    @Autowired
    private MyStreamChannel myStreamChannel;

    @Override
    public void sendMsg() {
        AppUser user = new AppUser();
        user.setId("10101");
        user.setNickname("imooc");

        // 消息通过绑定器发送给mq
        myStreamChannel.output()
                .send(MessageBuilder.withPayload(user).build());
    }

    @Override
    public void eat(String dumpling) {
        myStreamChannel.output()
                .send(MessageBuilder.withPayload(dumpling).build());
    }
}
service-article  com/imooc/article/stream/MyStreamConsumer.java
package com.imooc.article.stream;

import com.imooc.pojo.AppUser;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Component;

/**
 * 构建消费端
 */
@Component
@EnableBinding(MyStreamChannel.class) //开启通道绑定
public class MyStreamConsumer {

    /**
     * 监听并且实现消息的消费和相关业务处理
     */
    @StreamListener(MyStreamChannel.INPUT)
    public void receiveMsg(AppUser user) {
        System.out.println(user.toString());
    }

    @StreamListener(MyStreamChannel.INPUT)
    public void receiveMsg(String dumpling) {
        System.out.println(dumpling);
    }
}
service-article  com/imooc/article/controller/HelloController.java

@RestController
@RequestMapping("producer")
public class HelloController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private StreamService streamService;

    @GetMapping("/stream")
    public Object stream() {
        streamService.sendMsg();

        for (int i = 0 ; i < 10 ; i ++ ) {
            streamService.eat("我吃了第" + (i+1) + "只饺子~");
        }

        return "ok~~!!!";
    }

消息分组与持久化【stream】

避免重复消费 分组group 组内消费者不会重复消费

Stream消息分组
service-article  application.yml  
  cloud:
    stream:
      bindings:                    # 绑定通道和交换机
        myOutput:                   # 定义生产者的通道
          # 自定义交换机的名字,也就是代码里构建的信息,交给底层mq的交换机
          destination:
        myInput:                    # 定义消费者通道
          # 自定义交换机的名字,也就是消息从底层mq输入到消费端进行消费
          destination:
          group: boys

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

service-user  application.yml
  cloud:
    stream:
      bindings: # 绑定通道和交换机
        myOutput: # 定义生产者的通道
          # 自定义交换机的名字,也就是代码里构建的信息,交给底层mq的交换机
          destination:
        myInput: # 定义消费者通道
          # 自定义交换机的名字,也就是消息从底层mq输入到消费端进行消费
          destination:
          group: girls
service-article  com/imooc/article/stream/StreamService.java
package com.imooc.article.stream;

public interface StreamService {
  //public void sendMsg();
    public void eat(String dumpling);
}
service-article  com/imooc/article/stream/StreamServiceImpl.java
package com.imooc.article.stream;

import com.imooc.pojo.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

/**
 * 开启绑定器
 * 绑定通道channel
 */
@Component
@EnableBinding(MyStreamChannel.class)
public class StreamServiceImpl implements StreamService {

    @Autowired
    private MyStreamChannel myStreamChannel;

    @Override
 /* public void sendMsg() {
        AppUser user = new AppUser();
        user.setId("10101");
        user.setNickname("imooc");

        // 消息通过绑定器发送给mq
        myStreamChannel.output()
                .send(MessageBuilder.withPayload(user).build());
    } */

    @Override
    public void eat(String dumpling) {
        myStreamChannel.output()
                .send(MessageBuilder.withPayload(dumpling).build());
    }
}
service-article  com/imooc/article/stream/MyStreamConsumer.java
package com.imooc.article.stream;

import com.imooc.pojo.AppUser;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Component;

/**
 * 构建消费端
 */
@Component
@EnableBinding(MyStreamChannel.class) //开启通道绑定
public class MyStreamConsumer {

    /**
     * 监听并且实现消息的消费和相关业务处理
     */
 /* @StreamListener(MyStreamChannel.INPUT)
    public void receiveMsg(AppUser user) {
        System.out.println(user.toString());
    } */

    @StreamListener(MyStreamChannel.INPUT)
    public void receiveMsg(String dumpling) {
        System.out.println(dumpling);
    }
}
service-article  com/imooc/article/controller/HelloController.java

@RestController
@RequestMapping("producer")
public class HelloController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private StreamService streamService;

    @GetMapping("/stream")
    public Object stream() {
     // streamService.sendMsg();

        for (int i = 0 ; i < 10 ; i ++ ) {
            streamService.eat("我吃了第" + (i+1) + "只饺子~");
        }

        return "ok~~!!!";
    }

消息不会被重复消费
service-article:8001 我吃了1-10只饺子~ 【消费的饺子总数一共10次】
service-user:8003(女生) 我吃了1 3 5 7 9只饺子~ 【饺子随机】
service-user:8013(男生) 我吃了2 4 6 8 10只饺子~ 【饺子随机】

如果把service-user:8003(女生) service-user:8013(男生) 服务stop 用户微服务无法接收任何消息
但是我们定义了group → 消息是可以持久化的 当重启用户微服务之后 就会打印出刚刚已经吃的饺子了
服务器宕机 = 吃饺子中途去上厕所 回来后仍然还能吃到饺子

链路追踪概述与zipkin【sleuth】组件

链路追踪
  • Sleuth
  • 贯穿整个微服务系统中,追踪一个请求的过程
  • zipkin 可视化控制面板

下载zipkin-server-2.12.6-exec.jar
CMD → C:\Users\Pluminary>java -jar /Users/Pluminary/Desktop/zipkin-server-2.12.6-exec.jar
https://localhost:9411/zipkin/

整合zipkin【sleuth】项目入口是网关

zuul-server  pom.xml  +  service-article  pom.xml  +  service-user  pom.xml
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
zuul-server与service-article与service-user  application.yml
  zipkin:
    # 配置zipkin采集的服务地址,数据会发送到这里
    base-url: http://192.168.1.2:9411/
    sender:
      # 数据采集的传输通信方式,web http的形式
      type: web
  sleuth:
    sampler:
      # 数据采样比例(百分数),0~1
      probability: 1

https://localhost:9411/zipkin/

SpringCloud章节总结

  • eureka 注册中心
  • ribbon 负载均衡
  • feign 声明式客户端
  • hystrix 熔断降级组件
  • zuul 网关
  • config 配置中心
  • bus 消息总线
  • stream 消息驱动
  • zipkin + sleuth 链路追踪
阅读全文

Zookeeper+Dubbo应用与面试

2024/5/4

Zookeeper+Dubbo与面试周介绍

  • Zookeeper的下载、配置与运行
  • 数据结构node与常用命令
  • Watcher机制和权限cal介绍
  • 使用Zookeeper的Java原生客户端和Curator进行开发
  • RPC调用,在Dubbo架构下各服务的关系
  • 整合Dubbo和Zookeeper
  • 完成Dubbo开发案例
  • 线程进阶面试
  • 分布式、微服务面试题
  • Spring Cloud、Zookeeper的理解

Zookeeper

  • 理解Zookeeper
  • 安装、配置
  • 节点znode
  • 常用命令
  • Watcher机制
  • ACL权限控制
  • 代码实操

理解Zookeeper

  • 5大特点
  • 集群架构
  • Zookeeper和CAP关系
  • Zookeeper作用

为什么需要Zookeeper

  • 用起来像单机但是又比单机更可靠

  • leader在团队里的协调作用

  • 内存、单机

  • 集群、可靠

  • 当信息还没同步完成时,不对外提供服务

  • 同步的时间压缩的更短

Zookeeper诞生历史

雅虎研究室

  • 无单点问题的分布式协调架构,精力集中在处理业务逻辑
  • 内部很多项目都是使用动物的名字来命名
  • 大型动物园

Zookeeper是什么 [底层是Java]

  • Zookeeper是开源的高性能的分布式应用协调系统,一个高性能的分布式数据一致性解决方案

5大特点

  • 顺序一致性
  • 原子性 [全部成功或者全部不成功]
  • 单一视图 [无论连接哪个 都是一致的信息]
  • 可靠性
  • 及时性 [一定时间内能从服务器读到状态]
架构图、集群、工作过程

Zookeeper和CAP的关系

  • CP:一致性+分区容错性
  • 得到一致的数据结果,同时系统对网络具备容错性
  • 但是它不能保证每次服务请求的可用性
作用
  • 分布式服务注册与订阅
  • 统一配置文件
  • 生成分布式唯一ID [/order-0000001、/order-0000002]
  • Master节点选举

针对不能同时进行写数据,保证互斥同步 → 分布式锁

Zookeeper的安装、配置

  • 寻找教辅里的apache-zookeeper-3.6.0-bin.tar
  • 解压压缩包:tar zxvf apache-zookeeper-3.6.0-bin.tar.gz
  • 进入压缩包:cd apache-zookeeper-3.6.0-bin
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ls
bin  conf  docs  lib  LICENSE.txt  NOTICE.txt  README.md README_packaging.md
  • 配置[进入文件]:cp conf/zoo_sample.cfg conf/zoo
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# cp conf/zoo_sample.cfg conf/zoo
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ls conf/
configuration.xsl  log4j.properties  zoo  zoo.cfg  zoo_sample.cfg
  • 打开文件, 进行修改:vi conf/zoo.cfg
找到 dataDir=/tmp/zookeeper
修改成 dataDir=/tmp/lib/zookeeper 以免被自动清除
  • 启动:./bin/zkServer.sh start
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... already running as process 1877.
======================================================================
[root@iZbp1dssknxftmjczbtpndZ bin]# ./zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... ^[[ASTARTED
[root@iZbp1dssknxftmjczbtpndZ bin]# ./zkServer.sh status
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: standalone
======================================================================
  • 停止:./bin/zkServer.sh stop
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh stop
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED

znode节点 [基本数据模型]

  • 树结构

节点性质
  • 树形结构,也可以理解为linux的文件目录
  • 每一个节点都是znode,里面可以包含数据,也可以有子节点
  • 点分为永久节点临时节点(与客户端绑定) [session失效,也就是客户端断开过,临时节点消失]
  • 每个znode都有版本号,每当数据变化,版本号都会累加(乐观锁)
  • 删除或修改节点,版本号不匹配的话(版本号已超时), 会报错)
  • 每个节点存储的数据不宜过大,几k即可 [保存路径再去查询]
  • 节点可以设置权限,来限制用户的访问
  • Zookeeper保证读和写都是原子操作,且每次读写操作都是对数据的完整读取或完整写入
节点类型
  • 持久节点
  • 临时节点
  • 顺序节点
节点属性
  • dataVersion
  • cversion [child]
  • aclVersion [权限]

常用命令

  • 启动:./bin/zkServer.sh start

  • 连接到Zookeeper

    [root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
    /usr/bin/java
    ZooKeeper JMX enabled by default
    Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
    Starting zookeeper ... STARTED
    
    [root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkCli.sh -server 127.0.0.1:2181
    
  • 查看节点

    [zk: 127.0.0.1:2181(CONNECTED) 3] ls 
    ls [-s] [-w] [-R] path
    [zk: 127.0.0.1:2181(CONNECTED) 4] ls /
    [zookeeper]
    [zk: 127.0.0.1:2181(CONNECTED) 5] ls /zookeeper
    [config, quota]
    [zk: 127.0.0.1:2181(CONNECTED) 6] 
    
    • 查看节点状态:stat /
    [zk: 127.0.0.1:2181(CONNECTED) 6] stat /
    cZxid = 0x0
    ctime = Thu Jan 01 08:00:00 CST 1970
    mZxid = 0x0
    mtime = Thu Jan 01 08:00:00 CST 1970
    pZxid = 0x0
    cversion = -1    //子节点更改的次数
    dataVersion = 0  //数据更改的情况
    aclVersion = 0   //权限修改的情况
    ephemeralOwner = 0x0  //[0是永久节点 其他的是临时节点]
    dataLength = 0
    numChildren = 1  //有几个子节点
    
    • 查看节点的数据和状态:get
    [zk: 127.0.0.1:2181(CONNECTED) 7] get /45
    jj
    
    • 创建、修改、删除节点
    [zk: 127.0.0.1:2181(CONNECTED) 8] create
    create [-s] [-e] [-c] [-t ttl] path [data] [acl]
    
    //创建
    [zk: 127.0.0.1:2181(CONNECTED) 9] create /imooc2
    Created /imooc2
    [zk: 127.0.0.1:2181(CONNECTED) 10] create /imooc3 123
    Created /imooc3
    [zk: 127.0.0.1:2181(CONNECTED) 11] get /imooc3
    123
    
    [zk: 127.0.0.1:2181(CONNECTED) 12] stat /imooc3
    cZxid = 0x5
    ctime = Sun May 05 01:41:21 CST 2024
    mZxid = 0x5
    mtime = Sun May 05 01:41:21 CST 2024
    pZxid = 0x5
    cversion = 0
    dataVersion = 0
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 3
    numChildren = 0
    
    //修改
    [zk: 127.0.0.1:2181(CONNECTED) 13] set /imooc3 456
    [zk: 127.0.0.1:2181(CONNECTED) 14] get /imooc3
    456
    [zk: 127.0.0.1:2181(CONNECTED) 15] stat /imooc3
    cZxid = 0x5
    ctime = Sun May 05 01:41:21 CST 2024
    mZxid = 0x6
    mtime = Sun May 05 01:42:16 CST 2024
    pZxid = 0x5
    cversion = 0
    dataVersion = 1
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 3
    numChildren = 0
    

高级命令

创建节点的高级功能

  • 创建顺序节点 [-s] (会戴上序号)
[zk: 127.0.0.1:2181(CONNECTED) 16] create /imooc4
Created /imooc4
[zk: 127.0.0.1:2181(CONNECTED) 17] create -s /imooc4 /s
Created /imooc40000000003
[zk: 127.0.0.1:2181(CONNECTED) 18] create -s /imooc4 /s
Created /imooc40000000004
  • 临时节点

    ephemeralOwner = 0x0 [0是永久节点 其他的是临时节点]

[zk: 127.0.0.1:2181(CONNECTED) 22] create /imooc
Created /imooc
[zk: 127.0.0.1:2181(CONNECTED) 23] create -e /imooc/tmp 123
Created /imooc/tmp
[zk: 127.0.0.1:2181(CONNECTED) 24] stat /imooc/tmp
cZxid = 0xd
ctime = Sun May 05 01:48:50 CST 2024
mZxid = 0xd
mtime = Sun May 05 01:48:50 CST 2024
pZxid = 0xd
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x10085ad02c90001  //[0x0是永久节点 其他的是临时节点]
dataLength = 3
numChildren = 0
  • 乐观锁
[zk: 127.0.0.1:2181(CONNECTED) 27] set /imooc 6
[zk: 127.0.0.1:2181(CONNECTED) 28] get /imooc
6
[zk: 127.0.0.1:2181(CONNECTED) 29] stat /imooc
cZxid = 0xc
ctime = Sun May 05 01:48:46 CST 2024
mZxid = 0xe
mtime = Sun May 05 01:51:15 CST 2024
pZxid = 0xd
cversion = 1
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 1
numChildren = 1 
//set -v 1 /imooc 9 是因为上面 dataVersion = 1  指定条件版本更新
[zk: 127.0.0.1:2181(CONNECTED) 35] set -v 1 /imooc 9  
[zk: 127.0.0.1:2181(CONNECTED) 36] get /imooc
9
  • 删除命令
[zk: 127.0.0.1:2181(CONNECTED) 38] delete
delete [-v version] path //也可以按照版本号去删除
[zk: 127.0.0.1:2181(CONNECTED) 42] ls /
[imooc, imooc2, imooc3, imooc4, imooc40000000003, imooc40000000004, imooc40000000005, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 43] delete /imooc40000000003
[zk: 127.0.0.1:2181(CONNECTED) 44] ls /
[imooc, imooc2, imooc3, imooc4, imooc40000000004, imooc40000000005, zookeeper]
[zk: 127.0.0.1:2181(CONNECTED) 45] 

Watcher机制

  • 触发器、监督者
  • 使用场景:统一资源配置 [发生变化时 会給所有监听客户端发送信息]
Watcher事件类型
EventType 触发条件
NodeCreated (节点创建) Watcher监听的对应数据节点被创建
NodeDeleted (节点删除) Watcher监听的对应数据节点被删除
NodeDataChanged(节点数据修改) Watcher监听的对应数据节点的数据内容发生变更
NodeChildrenChanged(子节点变更) Watcher监听的对应数据节点的子节点列表发生变更
ACL
  • access control list 权限控制
  • 它使用权限位来允许/禁止对话节点及其所作用域的各种操作
  • ACL仅与特定的znode有关,与子节点无关
Scheme
  • ACL:[scheme采用的权限机制:id用户:permissions权限组合字符串]
  • world
  • auth [认证登录]
  • digest [密文加密]
  • ip [只允许特定ip访问]
  • super [超级权限]
权限字符串crdwa
  • Create
  • Read
  • Delete
  • Write
  • Admin
[ACL]权限使用场景
  • 区分开发/测试/运维环境,防止误操作
  • 可以针对不同IP而产生具体的配置,更安全

Java原生客户端连接到zookeeper [ZK]

  • 利用ZK原生的Java的API
  • 利用Apache Curator作为客户端来操作ZK
pom.xml
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>zk-practicer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.0</version>
        </dependency>
    </dependencies>

</project>
com/imooc/zkjavaapi/ZKConnect.java
package com.imooc.zkjavaapi;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.omg.CORBA.TIMEOUT;

import java.io.IOException;

/**
 * 连接到ZK服务端,打印连接状态
 */
public class ZKConnect implements Watcher {
    public static final String SERVER_PATH="47.98.225.105:2181";
    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args) throws IOException, InterruptedException {
        //后面new的相当于把这个作为参数传递进去
        //客户端和服务端是异步连接,连接成功之后,客户端会收到watcher通知
        //connectString:服务器的IP+端口号
        //sessionTImeout:超时时间
        //watcher:接收通知事件
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKConnect());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);
        System.out.println(zk.getState());
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        System.out.println("收到了通知" + watchedEvent);
    }
}
===========================================================
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.free=466MB
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.max=7209MB
17:35:46 INFO zookeeper.ZooKeeper: Client environment:os.memory.total=487MB
17:35:46 INFO zookeeper.ZooKeeper: Initiating client connection, connectString=127.0.0.1:2181 sessionTimeout=5000 watcher=com.imooc.zkjavaapi.ZKConnect@7591083d
17:35:46 INFO common.X509Util: Setting -D jdk.tls.rejectClientInitiatedRenegotiation=true to disable client-initiated TLS renegotiation
17:35:46 INFO zookeeper.ClientCnxnSocket: jute.maxbuffer value is 1048575 Bytes
17:35:46 INFO zookeeper.ClientCnxn: zookeeper.request.timeout value is 0. feature enabled=false
客户端开始连接ZK服务器了
CONNECTING
17:35:46 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:35:46 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:35:46 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:50517, server: 127.0.0.1/127.0.0.1:2181
17:35:46 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710001, negotiated timeout = 5000
收到了通知WatchedEvent state:SyncConnected type:None path:null
CONNECTED
log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} %p %c{2}: %m%n
[root@iZbp1dssknxftmjczbtpndZ apache-zookeeper-3.6.0-bin]# ./bin/zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.6.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... already running as process 4147.

windows环境下安装zookeeper教程详解(单机版)_windows zooke-CSDN博客

用代码对节点进行操作

com/imooc/zkjavaapi/ZKOperator.java
package com.imooc.zkjavaapi;

import java.io.IOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

/**
 * 描述:     演示对节点的操作,包含创建、读取、删除等。
 */
public class ZKOperator implements Watcher {

    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args)
            throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKOperator());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);

        /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
        System.out.println(zk.create("/imooc-create-node2", "imooc2".getBytes(), Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT));
    }

    @Override
    public void process(WatchedEvent event) {
    }
}
==========================================================================
客户端开始连接ZK服务器了
CONNECTING
17:57:14 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:57:14 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:57:14 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:57443, server: 127.0.0.1/127.0.0.1:2181
17:57:14 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710008, negotiated timeout = 5000
/imooc-create-node2
==========================================================================
==========================================================================
 /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
//        System.out.println(zk.create("/imooc-create-node2", "imooc2".getBytes(), Ids.OPEN_ACL_UNSAFE,
//                CreateMode.PERSISTENT));
//        zk.setData("/imooc-create-node", "imooc3".getBytes(), 1);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data));
    }
==========================================================================
客户端开始连接ZK服务器了
CONNECTING
17:58:09 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
17:58:09 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
17:58:09 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:57766, server: 127.0.0.1/127.0.0.1:2181
17:58:09 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec08710009, negotiated timeout = 5000
imooc2
version版本不一样 保证不做修改
com/imooc/zkjavaapi/ZKOperator.java
修改值 让版本变成1
 zk.setData("/imooc-create-node2", "imooc3".getBytes(), 1);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data)); 
----------------------------------------------------------------------------
Exception in thread "main" org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for /imooc-create-node2
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:122)
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:54)
    at org.apache.zookeeper.ZooKeeper.setData(ZooKeeper.java:2551)
    at com.imooc.zkjavaapi.ZKOperator.main(ZKOperator.java:41)

报错=>版本不一致
===========================================================================
[修改]
 zk.setData("/imooc-create-node2", "imooc3".getBytes(), 0);
        byte[] data = zk.getData("/imooc-create-node2", null, null);
        System.out.println(new String(data));
----------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:01:35 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:01:35 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:01:35 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:58870, server: 127.0.0.1/127.0.0.1:2181
18:01:35 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec0871000d, negotiated timeout = 5000
imooc3 [修改成功]
删除节点 [引入回调函数+休眠]
com/imooc/zkjavaapi/ZKOperator.java
package com.imooc.zkjavaapi;

import java.io.IOException;

import com.imooc.zkjavaapi.callback.DeleteCallBack;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

/**
 * 描述:     演示对节点的操作,包含创建、读取、删除等。
 */
public class ZKOperator implements Watcher {

    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    public static void main(String[] args)
            throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKOperator());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);

        /**
         * path:创建的路径
         * data:存储的数据
         * acl:权限,开放
         * createMode:永久、临时、顺序。
         */
        zk.create("/imooc-create-node3", "imooc3".getBytes(), Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
//        zk.setData("/imooc-create-node2", "imooc3".getBytes(), 0);
//        byte[] data = zk.getData("/imooc-create-node2", null, null);

        String ctx = "删除成功"; //把ctx的内容代入到DeleteCallBack()里面去运行
        zk.delete("/imooc-create-node3",0,new DeleteCallBack(),ctx);
        Thread.sleep(2000);
//        System.out.println(new String(data));
    }

    @Override
    public void process(WatchedEvent event) {
    }
}
--------------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:10:01 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:10:01 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:10:01 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:2600, server: 127.0.0.1/127.0.0.1:2181
18:10:01 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x10004ec0871000f, negotiated timeout = 5000
删除节点/imooc-create-node3
删除成功
com/imooc/zkjavaapi/callback/DeleteCallBack.java
package com.imooc.zkjavaapi.callback;

import org.apache.zookeeper.AsyncCallback;

/**
 * 删除后运行的方法
 */
public class DeleteCallBack implements AsyncCallback.VoidCallback {
    @Override
    public void processResult(int rc, String path, Object ctx) {
        System.out.println("删除节点" + path);
        System.out.println((String)ctx);
    }
}

处理Watcher事件

com/imooc/zkjavaapi/ZKGetNode.java
package com.imooc.zkjavaapi;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

/**
 * 和节点相关:是否存在,获取数据,加上Watch
 */
public class ZKGetNode implements Watcher {
    public static final String SERVER_PATH = "127.0.0.1:2181";

    public static final Integer TIMEOUT = 5000;

    //这个是门闩
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        /**
         * 客户端和服务端他们是异步连接,连接成功之后,客户端会收到watcher通知。
         * connectString:服务器的IP+端口号,比如127.0.0.1:2181
         * sessionTimeout:超时时间
         * watcher:通知事件
         */
        ZooKeeper zk = new ZooKeeper(SERVER_PATH, TIMEOUT, new ZKGetNode());
        System.out.println("客户端开始连接ZK服务器了");
        System.out.println(zk.getState());
        Thread.sleep(2000);
        System.out.println(zk.getState());

//        Stat exists = zk.exists("/imooc-create-node", false);//不需要额外监听
//        if (exists != null){
//            System.out.println("节点的版本为: "+exists.getVersion());
//        }else{
//            System.out.println("该节点不存在");
//        }
        zk.getData("/imooc-create-node", true, null);
        countDownLatch.await();
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.NodeChildrenChanged){
            System.out.println("数据被改变");
            countDownLatch.countDown();
        }
        System.out.println("收到了通知" + event);
    }
}
--------------------------------------------------------------------------------
在运行的情况下 去cmd中 修改
[zk: localhost:2181(CONNECTED) 1] set /imooc-create-node 11
--------------------------------------------------------------------------------
客户端开始连接ZK服务器了
CONNECTING
18:52:14 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
18:52:14 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
18:52:14 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:16204, server: 127.0.0.1/127.0.0.1:2181
18:52:14 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x1000534afde0001, negotiated timeout = 5000
收到了通知WatchedEvent state:SyncConnected type:None path:null
CONNECTED
数据被改变
收到了通知WatchedEvent state:SyncConnected type:NodeDataChanged path:/imooc-create-node

用Curator操作ZK

原生的Java的API的缺点
  • 不支持连接超时后的自动连接
  • Watcher注册一次后会失效
  • 不支持递归创建节点
利用Apache Curator
  • 解决了Watcher注册一次后会失效的问题
  • API更加简单易用,提供了工具类
com/imooc/curator/CuratorTests.java
package com.imooc.curator;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;

/**
 * 用Curator来操作ZK
 */
public class CuratorTests {
    public static void main(String[] args) throws Exception {
        String connectString = "127.0.0.1:2181";
        RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retry);
        client.start();
        String path = "/curator";
        String data = "test";
        client.create().withMode(CreateMode.PERSISTENT).forPath(path,data.getBytes());
        byte[] bytes = client.getData().forPath(path);
        System.out.println(new String(bytes));
    }
}
更改高级一点!!!【添加+修改+删除】
com/imooc/curator/CuratorTests.java
package com.imooc.curator;

import com.sun.net.httpserver.Authenticator.Retry;
import java.text.MessageFormat;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher.Event.EventType;

/**
 * 描述:     用Curator来操作ZK
 */
public class CuratorTests {

    public static void main(String[] args) throws Exception {
        String connectString = "127.0.0.1:2181";
        String path = "/curator2";

        RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retry);
        client.start();
        client.getCuratorListenable().addListener((CuratorFramework c, CuratorEvent event) -> {
            switch (event.getType()) {
                case WATCHED:
                    WatchedEvent watchedEvent = event.getWatchedEvent();
                    if (watchedEvent.getType() == EventType.NodeDataChanged) {
                        System.out.println(new String(c.getData().forPath(path)));
                    }
            }
        });
        String data = "test";
        String data2 = "test2";
        //添加
        client.create().withMode(CreateMode.PERSISTENT).forPath(path, data.getBytes());

        byte[] bytes = client.getData().watched().forPath(path);
        System.out.println(new String(bytes));
        //更改
        client.setData().forPath(path, data2.getBytes());
        //删除
        client.delete().forPath(path);
        Thread.sleep(2000); //保证足够时间运行成功
    }
}
-------------------------------------------------------------------------------------
19:44:41 INFO zookeeper.ClientCnxn: Opening socket connection to server 127.0.0.1/127.0.0.1:2181.
19:44:41 INFO zookeeper.ClientCnxn: SASL config status: Will not attempt to authenticate using SASL (unknown error)
19:44:41 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /127.0.0.1:32984, server: 127.0.0.1/127.0.0.1:2181
19:44:41 INFO zookeeper.ClientCnxn: Session establishment complete on server 127.0.0.1/127.0.0.1:2181, session id = 0x1000534afde0005, negotiated timeout = 40000
19:44:41 INFO state.ConnectionStateManager: State change: CONNECTED
test
test2

Dubbo [RPC远程过程调用]

  • 初识Dubbo
  • RPC介绍
  • Dubbo工作原理
  • 案例实操:项目编写
  • 整合Dubbo和Zookeeper
  • 实现服务间调用

初始Dubbo

Dubbo是什么
  • 轻量级、高性能的RPC框架
  • 并不是要成为一个微服务的全面解决方案
  • 以Java语言而出名
Dubbo现状
  • 全称是Apache Dubbo
  • 微店、网易云音乐、滴滴、中国电信、中国人寿
  • star有30K+个,fork有20K+个
Dubbo的故事
  • 09年开始做,做的第一个版本
  • 10年初的时候,架构升级,Dubbo2.0
  • 开源
  • one company战略
  • 合到HSF去
  • 第3节点,捐给Apache
开源的理解
  • 共同成长、巨人的肩膀上
  • 演化慢、不断革新、很强大的生命力
  • 突破任何的束缚,突破任何的常规,包容和开放

RPC介绍

  • RPC ——远程过程调用
  • 早期单机时代:IPC
  • 网络时代:把IPC扩展到网络上,这就是RPC
  • 实现RPC很头疼,于是有了RPC框架
  • 调用其他机器上的程序和调用本地的程序一样方便
常见的RPC框架
  • 阿里的Dubbo
  • 新浪的Montan
  • Facebook的Thrift
  • 各个框架都有其各自的优缺点
HTTP和RPC对比
  • 普通话[通用] 与 方言[企业内部]
  • 普通话本质上也是一种方言,只不过它是官方的方言
  • 传输效率
    • RPC定制自己传输请求让传输效率更高
    • HTTP会包含一些无用的内容效率较低
  • 性能消耗,主要在于序列化和反序列化的耗时
  • 负载均衡

Dubbo工作原理

一旦注册中心的信息有变化的时候会主动推送信息
  • 服务容器负责启动,加载,运行服务提供者
  • 服务提供者在启动时,向注册中心注册自己提供的服务
  • 服务提供者在启动时,向注册中心订阅自己所需的服务
  • 注册中心返回服务提供者地址列表给消费者
  • 从提供者地址列表中,选一台提供者进行调用
  • 定期发送一次统计数据到监控中心

模块 说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的控制中心
Container 服务运行容器

服务提供者开发

案例实操
  • 引入依赖
  • 添加注解
  • 整合Dubbo和Zookeeper
pom.xml[dubbo-practice]
<?xml version="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">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>producer</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.imooc</groupId>
    <artifactId>dubbo-practice</artifactId>
    <version>0.0.1</version>
    <name>dubbo-practice</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.1.12.RELEASE</spring-boot.version>
        <dubbo.version>2.7.4.1</dubbo.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- Apache Dubbo  -->
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-dependencies-bom</artifactId>
                <version>${dubbo.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo</artifactId>
                <version>${dubbo.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>javax.servlet</groupId>
                        <artifactId>servlet-api</artifactId>
                    </exclusion>
                    <exclusion>
                        <groupId>log4j</groupId>
                        <artifactId>log4j</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>
pom.xml[dubbo-practice-producer]
<?xml version="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>dubbo-practice</artifactId>
        <groupId>com.imooc</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>producer</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Dubbo Spring Boot Starter -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>
        <!-- Zookeeper dependencies -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-dependencies-zookeeper</artifactId>
            <version>${dubbo.version}</version>
            <type>pom</type>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Web 功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- MySQL connector, 需要与 MySQL 版本对应 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- MyBatis依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
    </dependencies>
</project>
com/imooc/producer/service/CourseListService.java
package com.imooc.producer.service;

import com.imooc.producer.entity.Course;

import java.util.List;

/**
 * 课程列表服务
 */
public interface CourseListService {
    List<Course> getCourseList();
}
com/imooc/producer/entity/Course.java
package com.imooc.producer.entity;

import java.io.Serializable;

/**
 * 描述:     Course实体类
 */
public class Course implements Serializable {

    Integer id;
    Integer courseId;
    String name;
    //1上架,0下架
    Integer valid;

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", name='" + name + '\'' +
                ", valid=" + valid +
                '}';
    } Getter+Setter
}
com/imooc/producer/service/impl/CourseListServiceImpl.java
package com.imooc.producer.service.impl;

import com.imooc.producer.entity.Course;
import com.imooc.producer.mapper.CourseMapper;
import com.imooc.producer.service.CourseListService;
import java.util.List;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * 描述:     课程列表服务实现类
 */
@Service(version = "${demo.service.version}")
public class CourseListServiceImpl implements CourseListService {

    @Autowired
    CourseMapper courseMapper;

    public List<Course> getCourseList() {
        return courseMapper.findValidCourses();
    }
}
com/imooc/producer/mapper/CourseMapper.java
package com.imooc.producer.mapper;

import com.imooc.producer.entity.Course;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * Mapper类
 */
@Mapper
@Repository
public interface CourseMapper {
    @Select("SELECT * FORM course WHERE valid = 1")
    List<Course> findValidCourses();
}
application.properties
demo.service.version=1.0.0

#server.port=8081

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_prepare?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}


spring.application.name=course-list

#dubbo协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
#dubbo注册
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.registry.file=${user.home}/dubbo-cache/${spring.application.name}/dubbo.cache

mybatis.configuration.map-underscore-to-camel-case=true

dubbo.scan.base-packages=com.imooc.producer.service.impl
com/imooc/producer/DubboProducerApplication.java
package com.imooc.producer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 描述:     Spring Boot启动类
 */
@EnableAutoConfiguration
public class DubboProducerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboProducerApplication.class, args);
    }
}

服务消费方开发

查看PID为8080:netstat -ano|findstr 8080
杀死进程:taskkill /pid 查询的PID /f

pom.xml[dubbo-practice-consumer]
<?xml version="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>dubbo-practice</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>concumer</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <!-- Dubbo Spring Boot Starter -->
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>2.7.4.1</version>
    </dependency>

    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo</artifactId>
    </dependency>
    <!-- Zookeeper dependencies -->
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-dependencies-zookeeper</artifactId>
      <version>${dubbo.version}</version>
      <type>pom</type>
      <exclusions>
        <exclusion>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <!-- Web 功能 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- MySQL connector, 需要与 MySQL 版本对应 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- MyBatis依赖-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
    <dependency>
      <groupId>com.imooc</groupId>
      <artifactId>producer</artifactId>
      <version>0.0.1</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>
com/imooc/consumer/service/CoursePriceService.java
package com.imooc.consumer.service;

import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import java.util.List;

/**
 * 描述:     课程价格服务
 */
public interface CoursePriceService {

    CoursePrice getCoursePrice(Integer courseId);

    List<CourseAndPrice> getCoursesAndPrice();
}
com/imooc/consumer/service/impl/CoursePriceServiceImpl.java
package com.imooc.consumer.service.impl;

import com.imooc.consumer.dao.CoursePriceMapper;
import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import com.imooc.consumer.service.CoursePriceService;
import com.imooc.producer.entity.Course;
import com.imooc.producer.service.CourseListService;
import java.util.ArrayList;
import java.util.List;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 描述:     课程 价格服务
 */
@Service
public class CoursePriceServiceImpl implements CoursePriceService {

    @Autowired
    CoursePriceMapper coursePriceMapper;

    @Reference(version = "${demo.service.version}")
    CourseListService courseListService;

    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrices(courseId);
    }

    @Override
    public List<CourseAndPrice> getCoursesAndPrice() {
        List<CourseAndPrice> courseAndPriceList = new ArrayList<>();
        List<Course> courseList = courseListService.getCourseList();
        for (int i = 0; i < courseList.size(); i++) {
            Course course = courseList.get(i);
            if (course != null) {
                CoursePrice price = getCoursePrice(course.getCourseId());
                if (price != null && price.getPrice() > 0) {
                    CourseAndPrice courseAndPrice = new CourseAndPrice();
                    courseAndPrice.setId(course.getId());
                    courseAndPrice.setCourseId(course.getCourseId());
                    courseAndPrice.setName(course.getName());
                    courseAndPrice.setPrice(price.getPrice());
                    courseAndPriceList.add(courseAndPrice);
                }
            }
        }
        return courseAndPriceList;
    }
}
com/imooc/consumer/dao/CoursePriceMapper.java
package com.imooc.consumer.dao;

import com.imooc.consumer.entity.CoursePrice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 描述:     Mapper类
 */
@Mapper
@Repository
public interface CoursePriceMapper {

    @Select("SELECT * FROM course_price WHERE course_id = #{courseId}")
    CoursePrice findCoursePrices(Integer courseId);
}
com/imooc/consumer/entity/CourseAndPrice.java
package com.imooc.consumer.entity;

import java.io.Serializable;

/**
 * 描述:     CoursePrice实体类
 */
public class CourseAndPrice implements Serializable {

    Integer id;
    Integer courseId;
    String name;
    Integer price;

    @Override
    public String toString() {
        return "CourseAndPrice{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", name='" + name + '\'' +
                ", price=" + price +
                '}';
    } Getter+Setter
}
com/imooc/consumer/entity/CoursePrice.java
package com.imooc.consumer.entity;


import java.io.Serializable;

/**
 * 描述:     CoursePrice实体类
 */
public class CoursePrice implements Serializable {

    Integer id;
    Integer courseId;
    Integer price;
} Getter+Setter
com/imooc/consumer/controller/CoursePriceController.java
package com.imooc.consumer.controller;

import com.imooc.consumer.entity.CourseAndPrice;
import com.imooc.consumer.entity.CoursePrice;
import com.imooc.consumer.service.CoursePriceService;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 描述:CoursePriceController
 */
@RestController
public class CoursePriceController {

    @Autowired
    CoursePriceService coursePriceService;


    @GetMapping({"/price"})
    public Integer getCoursePrice(Integer courseId) {
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        if (coursePrice != null) {
            return coursePrice.getPrice();
        } else {
            return -1;
        }
    }


    @GetMapping({"/coursesAndPrice"})
    public List<CourseAndPrice> getcoursesAndPrice() {
        return coursePriceService.getCoursesAndPrice();
    }
}
application.properties
demo.service.version=1.0.0

server.port=8084

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}


spring.application.name=course-price

#dubbo协议
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
#dubbo注册
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.registry.file=${user.home}/dubbo-cache/${spring.application.name}/dubbo.cache
com/imooc/consumer/DubboConsumerApplication.java
package com.imooc.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 描述:     Spring Boot启动类
 */
@SpringBootApplication
public class DubboConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboConsumerApplication.class, args);
    }
}
案例实操总结
  • 自动检查zk和依赖的服务
  • dubbo.scan.base-packages配置
  • 实现服务间调用

面试课

  • Spring Boot常见面试题
  • 线程常见面试题
  • 分布式的面试题
  • Docker相关面试题
  • Nginx和Zookeeper相关面试题
  • RabbitMQ相关面试题
  • 微服务相关
  • 彩蛋:学习方法
  • 锁分类、死锁
  • HashMap和final
  • 单例模式
  • 面试避坑指南
  • 重要的软实力

Spring、Spring Boot和Spring Cloud的关系?

  • Spring最初利用IOCAOP解耦
  • 按照这种模式搞了MVC框架 [之后就配置太繁琐了]
  • 写了很多样板代码很麻烦,就有了Spring Boot
  • Spring Boot是在强大的Spring帝国发展起来的,发明Spring Boot是为了让人们更好更高效的使用Spring,Spring Boot理念是约定优于配置
  • Spring Cloud是在Spring Boot基础上诞生的 [一系列框架的有序集合]

Spring Boot如何配置多环境

  • 开发、测试、预发、生产

面试官你好,我这边平时是会使用多套环境,比如说”开发、测试、预发、生产”环境。
开发环境通常在本地,它所连接的数据库也是专门用于开发的,里面的数据也是一定情况下算出来的,因为并不需要在开发环境的情况下保证数据的完全精确,为了开发效率的提高,我们通常造一些模拟的数据,通常开发完后我们要把程序部署到测试环境,因为测试环境通常是公司所提供的服务器,而开发环境通常是我们本机,对于本机而言如果关闭或关机后别人就无法访问了,但是测试的同学工作时间不一定能和开发的同学一致,如果把程序关掉了他们就没办法测试了。我们需要给测试同学提供一套稳定的环境去测试。而且有的时候会同时开发多种功能,前一个功能开发完了需要去测试,这个时候就要去开发新的功能了,此时本地的代码已经发生了变化,如果把开发环境当成测试环境的话会发生很多问题,它实际测试的和我们想要测试的不是同一套代码,正是这个原因测试环境是必不可少的,需要用一台稳定的服务器把我们开发好的部署上测试环境中去,这样的话无论电脑是否关机都不会影响测试人员的进度。但是在测试环境的数据库往往可以和开发环境的保持一致可以允许公用同一个数据。
预发环境是预备发布,和真正的线上环境高度统一,和测试环境的区别:
1.网络隔离 为了保证线上环境的稳定会采取环境隔离,在本地或者测试环境下是没有办法访问到预发环境的机器,不可直接访问。在预发环境通常采用真实的数据库去测试。在测试环境并不能把所有问题都测试出来,所以在测试环境中无法测试到的问题在预发环境就可以暴露出来了,有时候在测试环境中模拟的数据不是准确,比如模拟一个商品详情,报的是50个字,最后发现真实情况是100个字,就能看到数据库大小不够,再次比如测试的是整数,到真实环境中发现是小数。隔离+数据验真
生产环境是真实对外的数据,也会有很多流量进来,直接面向所有用户,也有并发问题,要确保数据稳定

  • 提供多套配置文件

在发布到某个环境之前,不建议把配置文件全部删除替换,有可能漏了文件导致了错误的替换,如果发布环境是测试环境的数据库,有可能会产生对外暴露的是测试环境的情况,这是很严重的事故

  • 通过改变application里的profiles.active值来加载对应的环境
com/imooc/profiles/ProfilesApplication.java
package com.imooc.profiles;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProfilesApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProfilesApplication.class, args);
    }
}
application.properties

spring.profiles.active=prod
application-pre.properties

spring.profiles.active=test
server.port=8082
application-prod.properties

spring.profiles.active=test
server.port=8083
application-test.properties

spring.profiles.active=test
server.port=8081

实际工作中,如何全局处理异常?

  • 为什么异常需要全局处理?不处理行不行?

如果我们不进行处理的话,异常可能会把整个堆栈抛出去,一旦发生异常,用户或者别用用心的黑客可以看到详细的异常发生情况,包含详细的错误信息和代码的行数,这样的话对方可以利用一个漏洞进行不同的尝试,而且可以顺藤摸瓜分析出更多潜在的风险,最终把系统攻击破,异常是必须处理的。
但为什么要全局处理呢?电商项目→exception→GlobalExceptionHandler

  • GlobalExceptionHandler [使用全局处理]
    识别到什么异常,调用什么其处理器。写了全局异常处理器,轻松的针对不同的异常做出定制化的解决方案,不但增加了安全性,对用户也是友好的
package com.imooc.mall.exception;

import com.imooc.mall.common.ApiRestResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.ArrayList;
import java.util.List;

/**
 * 19.处理统一异常的handler 业务异常 处理不同逻辑异常  20对密码进行MD5加密UserServiceImpl 先创建MD5Utils
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //   统一处理Exception.class异常 所有异常的父类
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception e) {
        log.error("Default Exception: ", e);
        return ApiRestResponse.error(ImoocMallExceptionEnum.SYSTEM_ERROR);
    }
    
    // 处理自己所定义的异常 用户/密码不能为空......
    @ExceptionHandler(ImoocMallException.class)
    @ResponseBody
    public Object handleImoocMallException(ImoocMallException e) {
        log.error("ImoocMallException: ", e); //传进来的是什么就传出去
        return ApiRestResponse.error(e.getCode(), e.getMessage());
    }

    //  39.处理方法参数不合规的情况
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ApiRestResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException: ", e);
        return handleBindingResult(e.getBindingResult());
    }
//  40.处理返回异常的ApiRespond 41去pom引入Swagger自动生成API文档
    private ApiRestResponse handleBindingResult(BindingResult result){
//  把异常处理为对外暴露的提示
        List<String> list = new ArrayList<>();
        if (result.hasErrors()){
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError objectError : allErrors) { //itli快速  对着for按alt+回车 改成增强for
                String message = objectError.getDefaultMessage();
                list.add(message);
            }
        }
        if (list.size() == 0){
            return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR);
        } //list.toString()生成所创建的异常描述信息
        return ApiRestResponse.error(ImoocMallExceptionEnum.REQUEST_PARAM_ERROR.getCode(), list.toString());
    }
}

线程如何启动?

Thread.start.run

  • 既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?

因为它只是一个普通的java代码,而不会真正的启动一个线程,调用一次run()方法只执行一次,而且是在主线程执行的,就没有起到任何创建线程的效果了。
如果选择start方法的话会在后台执行很多操作,比如去申请一个线程、让子方法去执行run()里的内容,而且还包括执行之后的对线程状态的调整。所以说表面上是相同都是执行一段代码,但是实际上是不同的。

  • 两次调用start()方法会出现什么情况?

两次调用start()方法会报异常,异常类型叫做IllegalThreadStateException,在start()的时候首先会进行线程状态的检测只有是new的时候才能正常启动,不允许启动两次

com/imooc/interniew/StartTwice.java
package com.imooc.interniew;

/**
 * 描述:     两次启动线程
 */
public class StartTwice {

    public static void main(String[] args) {
        Thread thread = new Thread();
        thread.start();
        thread.start();
    }
}
======================== 报错信息  ==========================
Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Thread.java:705)
    at com.imooc.interniew.StartTwice.main(StartTwice.java:11)
Thread.java
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

实现多线程的方法有几种?

  • 方法一:实现Runnable接口
com/imooc/interniew/createthreads/RunnableStyle.java
package com.imooc.interniew.createthreads;

import java.util.concurrent.Callable;

/**
 * 描述:     用Runnable方式创建线程
 */
public class RunnableStyle implements Runnable {
//new里new的意思是 把这个Runnable类作为参数传进Thread里面
    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("用Runnable方法实现线程");
    }
}
  • 方法二:继承Thread类
package com.imooc.interniew.createthreads;

import java.util.Timer;
import java.util.TimerTask;

/**
 * 描述:     利用定时器新建线程
 */
public class TimerDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }, 1000, 1000);
    }
}

两种方式的对比

方法1(实现Runnable接口更好)

实现多线程——常见面试问题
  • 实现Runnable接口和继承Thread类哪种方式更好?

    • 从代码架构角度

    本意是想让我们的执行类和任务的具体内容解耦,关系不那么密切,从架构角度好
    ★ Runnable具体描述的是工作的内容和线程的启动没有什么关系
    ★ Thread是维护整个线程的: 线程的启动、线程状态更改、线程结束,这两个本身的任务很分明的,不应该过度耦合[未来会发生很难扩展的问题]

    • 新建线程损耗

    ★ Runnable 在线程池更高级的用法中,一定不是每个任务都去新建一个线程的,为了提高整体的效率会让有限数量的线程由我们自己来确定,10个线程可以运行成千上万个任务。减少了新建线程的损耗。
    可以把任务作为一个参数直接传递给线程池,线程池里面用固定的线程去执行任务不需要每次都新建和销毁线程,这样大大降低了线程的开销。

    ★ Runnable 如果用这个不得不去把线程损耗承担起来,有的时候run方法执行的比较少,开销的少比不上新建线程的开销[捡了芝麻丢了西瓜]。

    • Java不支持双继承

    public class ThreadStyle extends Thread,Date (×)
    Class cannot extend multiple classes

    从语法的角度不允许继承多个类,一旦选定了一个父类就无法更改了[一辈子就被定死了]。在创建线程起就限制了代码的可扩展性,如果实现Runnable接口就不会出现这个问题,实现接口并不仅仅只能实现一个,实现接口后还可以继承类

    public class RunnableStyle extends Thread implements Runnable,Callable

两种方法的本质对比

方法一:最终调用target.run();

此方法本质是传入类后调用!
Ctrl+F12可以精确查找方法
@Override
public void run(){
if(target != null){
target.run()
}
}
而target是什么呢?实际上就是我们写的
@Override
public void run() {
System.out.println(“用Runnable方法实现线程”);
}

方法二:run()整个都被重写

整个重写代码

若同时使用这两种方法会发生什么?

  • 从面向对象的思想去考虑
com/imooc/interniew/createthreads/BothRunnableThread.java
package com.imooc.interniew.createthreads;
/**
 * 描述:     同时使用RUNNABLE和Thread两种方式实现线程
 */
public class BothRunnableThread {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("实现Runnable接口的方式");
            }
        }) {
            @Override
            public void run() {
                System.out.println("我来自Thread");
            }
        };
        t1.start();
    }
}
=========================================================
我来自Thread
---------------------------------------------------------
因为 run重写会被覆盖!!子类覆盖父类时 实行子类方法
@Override
public void run(){
    if(target != null){
        target.run()
    }
}
其他观点分析
  • 线程池创建线程也算是一种新建线程的方式 [把那两种方式进行包装]

  • 通过Callable创建线程,也算是一种新建线程的方式

  • 定时器[方法二:extends Thread]

    package com.imooc.interniew.createthreads;
    
    import java.util.Timer;
    import java.util.TimerTask;
    
    /**
     * 描述:     利用定时器新建线程
     */
    public class TimerDemo {
    
        public static void main(String[] args) {
            System.out.println(Thread.currentThread().getName());
            Timer timer = new Timer();
            timer.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            }, 1000, 1000);
        }
    }
    =======================================================
    main     //两个不一致证明新建了一个线程
    Timer-0
    Timer-0
    ......
    
  • 匿名内部类

  • Lambda表达式

实现多线程——常见面试问题
  • 有多少种实现线程的方法?5点思路
    • 不同的角度看,会有不同的答案
    • 经典答案是
    • 我们看原理,两种本质都是一样的
    • 具体展开说其他方式
总结:最精准的描述

准确地讲:创建线程只有一种方法那就是构造Thread类,而实现线程的执行单元有两种方式
★ 方法一:实现Runnable接口的run方法,并把Runnable实例传給Thread类
★ 方法二:重写Thread的run方法(继承Thread类)
多线程的实现方法,在代码种写法千变万化,但其本质万变不离其宗

线程的生命周期是什么?

线程有几种状态?
  • 有哪6种状态
  • 每个状态是什么含义?
  • 状态间的转化?
  • 阻塞状态是什么?
每个状态是什么含义?
  • New
  • Runnable [从new到调用start方法]
  • Blocked [线程状态由sychronized修饰]
  • Waiting
  • Timed Waiting
  • Terminated

状态转换的注意点和阻塞

com/imooc/interniew/NewRunnableTerminated.java
package com.imooc.interniew;

/**
 * 描述:     演示New、Runnable、Terminated状态。
 */
public class NewRunnableTerminated {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread();
        //打印出NEW的状态 线程被创建但没有启动会打出new状态
        System.out.println(thread.getState());
        thread.start();
        //打印出Runnable状态 线程被启动后打印runnable状态
        System.out.println(thread.getState());
        Thread.sleep(100);
        //打印出TERMINATED状态 打印terminate状态
        System.out.println(thread.getState());
    }
}
=================================================================================
NEW
RUNNABLE
TERMINATED

Process finished with exit code 0
com/imooc/interniew/BlockedWaitingTimedWaiting.java
package com.imooc.interniew;

/**
 * 描述:     展示Blocked、Waiting、Timed_Waiting状态
 */
public class BlockedWaitingTimedWaiting implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new BlockedWaitingTimedWaiting();
        Thread t1 = new Thread(runnable);
        t1.start();
        Thread t2 = new Thread(runnable);
        t2.start();
        Thread.sleep(10);
        //打印Timed_Waiting状态,因为正在执行Thread.sleep(1000);
        System.out.println(t1.getState());
        //打印出BLOCKED状态,因为t2拿不到synchronized锁[线程1还在休眠]
        System.out.println(t2.getState());

        Thread.sleep(1300);
        //打印出WAITING状态,以为执行了wait()
        System.out.println(t1.getState());
    }

    @Override
    public void run() {
        syn();
    }

    private synchronized void syn() { //锁!!
        try {
            Thread.sleep(1000);
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
=================================================================================
TIMED_WAITING
BLOCKED
WAITING
阻塞状态
  • 一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态
  • 不仅仅是Blocked

分布式面试题

什么是分布式
  • 饭店厨师的例子
    • 一个厨师
    • 多个厨师
    • 术业有专攻:配菜师、洗菜工
  • 实际项目的演进过程
    • 一个项目,大而全
    • 多台机器,部署同样的应用
    • 分布式:权限系统、员工系统、请假系统
分布式和单体结构哪个更好?[脱离业务场景和发展阶段的空谈就是耍流氓]
传统单体架构 分布式架构
新人的学习成本 业务逻辑成本高 架构逻辑成本高
部署、运维 容易 发布频繁、发布顺序复杂、运维难
隔离性 一损俱损,殃及鱼池 故障影响范围小

CAP理论是什么?[只选其二 三者不可兼得]

  • C(Consistency, 一致性):读操作是否总能读到前一个写操作的结果
  • A(Availability, 可用性):非故障节点应该在合理的时间内做出合理的响应
  • P(Partition tolerance, 分区容错性):当出现网络分区现象后,系统能够继续运行
CAP怎么选?
  • CP或者AP
  • 在什么场合,可用性高于一致性?

为什么需要Docker?

  • Docker:用来装程序以及环境的容器
  • 环境配置的难题
  • 虚拟机
Docker的用途是什么?
  • 提供统一的环境
  • 提供快速拓展、弹性伸缩的云服务
  • 防止其他用户的进程把服务器资源占用过多

Docker的架构是什么样的?

Docker的网络模式有哪些?
  • Bridge [桥接 用外面主机的端口号映射到里面的端口号 实现了一座桥]
  • Host [里面的容器不会获得独立的网络配置 不会使用虚拟网卡ip 而是使用宿主机上的ip和端口号]
  • None [不需要网络模式]

Nginx的适用场景有哪些?

  • HTTP的反向代理服务器

  • 动态静态资源分离

    • 不分离会变慢
    • 静态资源无需经过Tomcat,Tomcat只负责处理动态请求
    • 后缀为gif的时候,Nginx会直接获取到当前请求的文件并返回
    • 静态资源服务器

Nginx常用命令有哪些?

/usr/sbin/nginx 启动
-h 帮助
-c 读取指定的配置文件
-t 测试
-v 版本
-s信号
  stop 立即停止(不再接收任何请求立刻停止)  
  quit 优雅停止(不接收了但目前的请求要作完)
  reload 重启
  reopen 更换日志文件

Zookeeper有哪些节点类型?

  • 持久节点
  • 临时节点
  • 顺序节点

为什么要用消息队列?什么场景用?

  • 系统解耦
  • 异步调用
  • 流量削峰
消息队列RabbitMQ核心概念?

同一个RabbitMQ的Server下建立不同的虚拟主机,他们之间是相互独立的,用于不同的业务线。

交换机工作模式有哪4种?
  • fanout:广播,这种模式只需要将队列绑定到交换机上即可,是不需要设置路由键的

  • direct:根据RoutingKey匹配消息路由到指定队列 [消费者接收消息不一致]

  • topic:比如消息严重性怎么样、只想记录error模块的用户信息

    • ***** 可以替代一个单词

    • # 可以替代零个或多个单词

  • headers:根据发送消息内容中的headers属性来匹配

微服务面试题

微服务有哪两大门派?

  • Spring Cloud:众多子项目
  • dubbo:高性能、轻量级的开源RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
  • dubbo提供的能力只是SpringCloud的一部分子集
Spring Cloud核心组件有哪些?
核心组件 Spring Cloud
服务注册中心 Spring Cloud Netflix Eureka
服务调用 Spring Cloud Netflix Feign
服务网关 Spring Cloud Netflix Zuul
断路器 Spring Cloud Netflix Hystrix
画一下Eureka架构
  • EureKa Server 和 EureKa Client

  • 集群 [只要能获得一个Eureka Server 就能获得整个信息]

负载均衡的两种类型是什么?
  • 客户端负载均衡(Ribbon)
  • 服务端负载均衡(Nginx)
负载均衡有哪些策略?
  • RandomRule表示随机策略
  • RoundRobinRule表示轮询策略
  • ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
为什么需要断路器?

防止线程突然卡住,当发现某个模块不可用时,把它摘除不影响主要流程。

为什么需要网关?
  • 签名校验、登录校验冗余问题
  • 统一对外,安全 [对恶意IP进行拦截 打出日志]
Dubbo的工作流程是什么?

彩蛋:学习编程知识的优质路径

  • 宏观上
  1. 并不是靠工作年限,有的人工作5年技术却还是只懂皮毛
  2. 要有强大的责任心,不放过任何bug,找到原因并去解决,这就是提高
  3. 主动:永远不会觉得自己的时间多余,重构、优化、学习、总结等
  4. 敢于承担:虽然这个技术难题以前没有碰到过,但是在一定的了解调研后,敢于承担技术难题,让工作充满挑战,这一次次攻克难关的过程种,进步是飞速的
  5. 关心产品,关心业务,而不是只写代码
  • 微观上
  1. 系统化的学习 看经典的书籍
  2. 看官方文档
  3. 自己动手写代码,尝试应用到项目中
  4. 不理解的内容参考多个知识来源,综合判断
  5. 学习开源项目,总结代码

Synchronized和Lock

Lock简介、地址、作用
  • 锁是一种工具,用于控制对共享资源的访问
  • Lock和Synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
  • Lock并不是用来替代Synchronized的,而是当使用Synchronized不合适或不满足要求的时候,来提供高级功能
  • Lock接口最常见的实现类是ReentrantLock
  • lock()、tryLock()、tryLock(long time, TimeUnit unit)locakInterruptibly()
lock()
  • lock()就是最普通的获取锁。如果锁已经被其他线程获取,则进行等待
  • Lock不会像Synchronized一样在异常时自动释放锁
  • 因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
  • lock()方法不能被中断,这就会带来很大隐患:一旦陷入死锁,lock()就会陷入永久等待
tryLock()
  • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败
  • 相对比lock,这样的方法显然功能更加强大了,我们可以根据是否能获取到锁来决定后续程序的行为
  • 该方法会立即返回,即便在拿不到锁时不会一直在那等
tryLock(long time, TimeUnit unit):超时就放弃

locakInterruptibly():相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断

unlock():解锁 [最应该写在try…finally里面]

Synchronized和Lock有什么不同?

相同点:
  • 保障资源线程的安全:目的和作用都是为了 保障资源线程的安全
    [使用Synchronized后被保护的代码块最多只有一个线程可以访问]
  • 可重入 [不然就必须在获得第二个锁前释放]
com/imooc/interniew/Reentrant.java
package com.imooc.interniew;

/**
 * 描述:     synchronized可重入
 */
public class Reentrant {

    public synchronized void f1() {
        System.out.println("f1方法被运行了");
        f2();
    }

    public synchronized void f2() {
        System.out.println("f2方法被运行了");
    }

    public static void main(String[] args) {
        Reentrant reentrant = new Reentrant();
        reentrant.f1();
    }
}
========================================================
f1方法被运行了
f2方法被运行了
  • ReentrantLock [实现了Lock接口]
不同点:
  • 用法
    • Synchronized用在方法上、用在同步代码块上 [隐式]
    • Lock必须使用lock方法加锁unlock方法解锁 [显式]
  • 加解锁顺序不同
    • Synchronized是java内部控制,自动加解锁
    • Lock可以手动调节
  • Synchronized锁不够灵活
    • Synchronized获得了一个锁 其他的只能等待
    • Lock获得锁很灵活 可以随时调整
  • 性能区别
    • Synchronized由差到好 目前同等程度的性能

你知道有几种锁?

  • 共享锁[读写锁] 和 独占锁[排他锁]

  • 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据

  • 共享锁和排他锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁

读写锁的作用
  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
读写锁的规则
  • 多个线程只申请读锁,都可以申请到
  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
  • 一句话总结:要么是一个多个线程同时有读锁,要么一个线程有写锁,但是两者不会同时出现(要么多读,要麽一写)
公平锁 和 非公平锁
  • 公平指的是按照线程请求的顺序,来分配锁
  • 非公平指的是不完全按照请求的顺序,在一定情况下,可以插队
  • 注意:非公平也同样不提倡 ”插队“ 行为,这里的非公平,指的是”在合适的时机”插队,而不是盲目插队
  • 什么是合适的时机呢?
    • 买火车票被插队的例子,排队买的例子
  • 实际情况并不是这样的,java设计者这样设计的目的是为了提高效率
  • 避免唤醒带来的空档期,提升吞吐量
优势 劣势
公平锁 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 更慢,吞吐量更小
不公平锁 更快,吞吐量更大 有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行
悲观锁 和 乐观锁
  • 是否锁住资源的角度分类
悲观锁
  • 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失
  • java中悲观锁的实现就是synchronizedLock相关类
乐观锁
  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象
  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过,如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据
  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略
  • 乐观锁的实现一般都是利用CAS算法来实现的
在数据库中
  • select for update就是悲观锁
  • version控制数据库就是乐观锁
经典例子
添加一个字段lock_version
先查询这个更新语句的vesion:SELECT * FROM table
然后
UPDATE SET num = 2, 
version = version + 1 WHERE version = 1 AND id = 5
如果version被更新了等于2,不一样就会更新出错,这就是乐观锁的原理

自旋锁 和 非自旋锁
  • 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
  • 如果同步代码块的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
  • 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复线程的花费可能会让系统得不偿失
  • 如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁
  • 而为了让当前线程“稍等一下”,我们需要让当前线程进行自旋,如果在自旋完成后前面锁定同步资源资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。
自旋锁的缺点
  • 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
  • 在自旋的过程中,一直消耗CPU,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的
可重入锁 和 非可重入锁
  • 什么是可重入 [摇一个号拿N个牌]

  • 好处 [避免死锁]

可中断锁 和 不可中断锁
  • 可中断锁 [可以随时中断]

死锁相关

写一个必然死锁的例子?
什么是死锁?
  • 发生在并发

  • 互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁

  • 一图胜千言

  • 线程A持有锁1但试图获取锁2 线程B持有锁2但视图获取锁1

多个线程造成死锁的情况
  • 如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁
死锁的影响
  • 死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力
    • 数据库中:检测并放弃事务
    • JVM中:无法自动处理

几率不高但危害大
  • 不一定发生,但是遵守墨菲定律
  • 一旦发生,多是高并发场景,影响用户多
  • 整个系统崩溃、子系统崩溃、性能降低
  • 压力测试无法找出所有潜在的死锁
deadlock/DeadLock.java
package deadlock;

/**
 * 描述:     必然发生死锁
 */
public class DeadLock implements Runnable {

    public int flag;

    static Object o1 = new Object();
    static Object o2 = new Object();

    public void run() {
        System.out.println("开始执行");
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("成功获取到了两把锁");
                }
            }
        }
        if (flag == 2) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("成功获取到了两把锁");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock r1 = new DeadLock();
        DeadLock r2 = new DeadLock();
        r1.flag = 1;
        r2.flag = 2;
        new Thread(r1).start();
        new Thread(r2).start();
    }
}
====================================== 分析 =======================================
★ 当类的对象flag=1时(T1),先锁定O1,睡眠500毫秒,然后锁定O2;
★ 而T1在睡眠的时候另一个flag=2的对象(T2)线程启动,先锁定O2,睡眠500毫秒,等待T1释放O1;
★ T1睡眠结束后需要锁定O2才能继续执行,而此时O2已被T2锁定
★ T2睡眠结束后需要锁定O1才能继续执行,而此时O1已被T1锁定
★ T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁

哲学家就餐问题?

  • 先拿起左手的筷子
  • 然后拿起右手的筷子
  • 如果筷子被人使用了,那就等别人用完
while(true){ //伪代码
    think();
    pick_up_left_fork();
    pick_up_right_fork();
    eat();
    put_down_right_fork();
    put_down_left_fork();
}
package deadlock;

/**
 * 描述:     哲学家就餐问题导致的死锁
 */
public class DiningPhilosophers {

    public static class Philosopher implements Runnable {

        private Object leftChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        private Object rightChopstick;

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("Thinking");
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Picked up right chopstick - eating");
                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {//五个哲学家方便管理
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i]; //从0开始 i为5 所以底下要加1 但是越界就取余
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
            //领导调节(检测与恢复策略) [定期巡视命令哲学家] 让最后一个 跟别人不一样 
            //别人都是先左后右面 它是先后面再左边 避免了环路的形成
            //直接避免死锁发生!!!!!
            if (i == philosophers.length - 1) {
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            } else {
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            }
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}
=============================================================================
哲学家4号 Thinking
哲学家5号 Thinking
哲学家3号 Thinking
哲学家1号 Thinking
哲学家2号 Thinking
哲学家2号 Picked up left chopstick
哲学家3号 Picked up left chopstick
哲学家1号 Picked up left chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
哲学家4号 Put down right chopstick
哲学家4号 Put down left chopstick
哲学家4号 Thinking
哲学家3号 Picked up right chopstick - eating
哲学家3号 Put down right chopstick
哲学家3号 Put down left chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
哲学家4号 Put down right chopstick
哲学家4号 Put down left chopstick
哲学家3号 Thinking
哲学家2号 Picked up right chopstick - eating
哲学家4号 Thinking
哲学家2号 Put down right chopstick
哲学家4号 Picked up left chopstick
哲学家4号 Picked up right chopstick - eating
.............................
发生死锁的时候哲学家都拿着左边的筷子
原理:Thread.sleep((long) (Math.random() * 10));
random到了一个更大的数
多种解决策略
  • 服务员检查(避免策略) [提前看一看是否发生死锁]
  • 改变一个哲学家拿叉子的顺序(避免策略)
  • 餐票(避免策略)
  • 领导调节(检测与恢复策略) [定期巡视命令哲学家]
实际工程中如何避免死锁
① 设置超时时间
  • Lock的tryLock(long timeout, TImeUnit unit)
  • synchronized不具备尝试锁的能力
  • 造成超时的可能性很多:发生了死锁、线程陷入死循环、线程执行很慢
  • 获取锁失败:打日志、发报警邮件、重启
  • 代码演示:退一步海阔天空
package deadlock;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     用tryLock来避免死锁
 */
public class TryLockDeadlock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程1获取到了锁1");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程1获取到了锁2");
                            System.out.println("线程1成功获取到了两把锁");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
                            System.out.println("线程1尝试获取锁2失败,已重试");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程2获取到了锁2");

                        Thread.sleep(new Random().nextInt(1000));
                        if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程2获取到了锁1");
                            System.out.println("线程2成功获取到了两把锁");
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
                            System.out.println("线程2尝试获取锁1失败,已重试");
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
====================================================================================
线程1获取到了锁1
线程2获取到了锁2
线程1尝试获取锁2失败,已重试 【正是有了线程1的释放 才让线程2拿到了锁1】
线程2获取到了锁1
线程2成功获取到了两把锁
线程1获取到了锁1
线程1获取到了锁2
线程1成功获取到了两把锁
② 多使用并发类而不是自己设置锁
③ 尽量降低锁的使用粒度:用不同的锁而不是一个锁
④ 如果能使用同步代码块,就不使用同步方法:自己锁定锁对象
⑤ 给你的线程起个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践

Hashmap为什么[线程]不安全?

  • i++

    • 第一个步骤是读取
    • 第二个步骤是增加
    • 第三个步骤是保存

    有可能会发生线程不安全的情况

  • 同时put碰撞导致数据丢失

  • 可见性问题无法保证

final的作用是什么?有哪些用法?

  • final修饰变量
  • final修饰方法
  • final修饰
final的作用
  • 早期
    • 锁定
    • final效率:早期的Java实现版本中,会将final方法转为内嵌调用
  • 现在
    • 类防止被继承、方法防止被重写、变量防止被修改
    • 天生是线程安全的,而不需要额外的同步开销
final的3种用法
final修饰变量:赋值时机
  • 属性被声明为final后,该变量则只能被赋值一次。且一旦被赋值,final的变量就不能再被改变,无论如何也不会改变

  • final修饰变量

    • final instance variable (类中的final属性)

      • 第一种是在声明变量的等号右边直接赋值
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public static int a = 5;
      
          public static void main(String[] args) {
              FinalVariable.a = 8;
          }
      }
      
      • 第二种就是构造函数中赋值
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public final int a;
      
          public testFinal(int a) {
              this.a = b;
          }
          public static void main(String[] args) {
          }
      }
      
      • 第三种就是在类的初始代码块种赋值(不常用)
      package com.imooc.interniew;
      
      /**
       * 描述:     final修饰变量
       */
      public class FinalVariable {
      
          public static final int a;
      
          static {
              a = 9;
          }
      
          void testFinal() {
              final int b;
          }
          public static void main(String[] args) {
          }
      }
      
      • 如果不采用第一种赋值方法,那么就必须在第2、3种挑一个来赋值,而不能不赋值,这是final语法所 规定的
    • final static variable (类中的static final属性)

      • 两种赋值时机:除了在声明变量的等号右边直接赋值外,static final变量还可以用static初始代码块赋值,但是不能用普通的初始代码块赋值
    • final local variable (方法中的final变量)

      • 和前两种不同,由于这里的变量是在方法里的,所以没有构造函数,也不存在初始代码块
      • final local variable不规则赋值时机,只要求在使用前必须赋值,这和方法中的非final变量的要求也是一样的
    为什么要规定赋值时机?
    • 如果初始化不赋值,后续赋值,就是从null变成你的赋值,这就违反final不变的规则了
  • final修饰方法

    • 构造方法不允许final修饰
    • 不可被重写,也就是不能被override
    package com.imooc.interniew;
    
    /**
     * 描述:     final修饰方法
     */
    public class FinalMethodDemo{
    
        public void drink() {
        }
    
        public final void eat() {
        }
    }
    
    class SubClass extends FinalMethodDemo {
    
        @Override
        public void drink() {
            super.drink();
        }
    
    //    @Override
    //    public void eat() {
    //        super.drink();
    //    }
    }
    
  • final修饰

    • 不可被继承
    • 例如经典的String类就是final的,我们从见过哪个类是继承String类的

单例模式你会写吗?

  • 什么是单例模式

保证只有一个实例且提供只有一个全局入口

  • 为什么需要单例:节省内存和计算、保证结果正确、方便管理
  • 适用场景
    • 无状态的工具类
    • 全局信息类
  • 单例模式的8种写法
    • 饿汉式(静态常量) [可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     饿汉式(静态常量)(可用)
     */
    public class Singleton1 {
    
        private Singleton1() {
    
        }
    
        private final static Singleton1 INSTANCE = new Singleton1();
    // [未达到懒加载] 直接创建出来了
        public static Singleton1 getInstance() {
            return INSTANCE;
        }
    }
    
    • 饿汉式(静态代码块) [可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     饿汉式(静态代码块)(可用) // [未达到懒加载]
     */
    public class Singleton2 {
    
        private Singleton2() {
    
        }
    
        static {
            INSTANCE = new Singleton2();
        }
        private final static Singleton2 INSTANCE;
    
        public static Singleton2 getInstance() {
            return INSTANCE;
        }
    }
    
    • 懒汉式(线程不安全) [不可用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程不安全)
     */
    public class Singleton3 {
    
        private Singleton3() {
    
        }
    
        private static Singleton3 INSTANCE;
    
        public static Singleton3 getInstance() {
            if (INSTANCE == null) {//第一次访问方法
                INSTANCE = new Singleton3();//初始化
            }
            //此时如果两个线程同时访问,都是null,就创造了两个初始化 违反单例模式
            return INSTANCE; //已经被初始化 就返回
        }
    }
    
    • 懒汉式(线程安全,同步方法) [不推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton4 {
    
        private Singleton4() {
    
        }
    
        private static Singleton4 INSTANCE;
        //synchronized同步关键字 最多一个线程访问
        //不推荐用的原因是因为一旦适用了synchronized同步关键字 线程就要排队 并发量大
        public synchronized static Singleton4 getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new Singleton4();
            }
            return INSTANCE;
        }
    }
    
    //方法上不进行同步了
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton5 {
    
        private Singleton5() {
    
        }
    
        private static Singleton5 INSTANCE;
    
        public static Singleton5 getInstance() {
            if (INSTANCE == null) { //此时不会存在两个线程同时出来了
                synchronized (Singleton5.class) {
                    //假如第一个执行完了 第二个进去执行 那么结果还是生成了两个 不符合单例
                    INSTANCE = new Singleton5();
                }
            }
            return INSTANCE;
        }
    }
    
    • 双重检查[推荐用]
      • 新建一个对象,但还未初始化
      • 调用构造函数等来初始化该对象
      • 把对象指向引用
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     懒汉式(线程安全,同步方法)(不推荐)
     */
    public class Singleton6 {
    
        private Singleton6() {//2
    
        }
    
        private static volatile Singleton6 INSTANCE;
    
        public static Singleton6 getInstance() {
            if (INSTANCE == null) {
                synchronized (Singleton6.class) {
                    if (INSTANCE == null) { //3
                        //就不会出现多个结果了
                        INSTANCE = new Singleton6 //1
                    }
                }
            }
            return INSTANCE;
        }
    }
    
    • 静态内部类[推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     静态内部类写法(推荐用)
     */
    public class Singleton7 {
    
        private Singleton7() {
    
        }
    
        private static class SingletonInstance {
            private static Singleton7 INSTANCE = new Singleton7();
        }
    
        public static Singleton7 getInstance() {
            return SingletonInstance.INSTANCE;
        }
    }
    
    • 枚举[推荐用]
    package com.imooc.interniew.singleton;
    
    /**
     * 描述:     枚举单例模式
     */
    public enum Singleton8 {
        //1.写法简洁
        //2.线程安全
        //3.防止反射
        
        INSTANCE;
    }
    
不同写法对比
  • 饿汉:简单,但是没有lazy loading
  • 懒汉:有线程安全问题
  • 静态内部类:可用
  • 双重检查:面试用
  • 枚举:最好
单例模式面试常见问题
  • 饿汉式的缺点?[没有懒加载]
  • 懒汉式的缺点?[不可以保证线程安全]
  • 为什么要用double-check?不用就不安全吗?
  • 为什么双重检查模式要用volatile
  • 应该如何选择,用哪种单例的实现方案最好
    • 单元素的枚举类型已经成为实现Singleton的最佳方法
    • 写法简单
    • 线程安全有保障
    • 避免反射破坏单例

面试避坑指南

  • 何时投简历 [Offer数量只会越来越少,越早越好] 秋招7-9月 社招金3银4
  • 信息尽量全面
  • 技术栈契合
  • 慎用”精通“ [对源码有很多熟悉] => 多写熟悉
  • 面试无处不在 [如果没时间可以申请换一个时间节点 并询问对方是否有时间]
  • 提前调试设备
  • 仪容仪表、提前到场 [提前5分钟左右联系面试官]
  • 确认问题 [实在不会可以说思路设想]
  • 问面试官的问题 [提前查公司信息 我了解到我们公司… 可不可以介绍一下… 未来规划… 技术栈…]

哪些软素质值得面试官认可?

  • 基本能力:聆听、沟通表达、学习能力
  • 工作能力:协作执行力、管理能力
  • 个人素质:技术自驱力、韧性、积极开放的心态

面试课总结

  • Spring Boot常见面试题
  • 线程常见面试题
  • 分布式的面试题
  • Docker相关面试题
  • Nginx和Zookeeper相关面试题
  • RabbitMQ相关面试题
  • 微服务相关
  • 彩蛋:学习方法
  • 锁分类、死锁
  • HashMap和final
  • 单例模式[高频考点]
  • 面试避坑指南
  • 重要的软实力

2024.5.9 14:34   在创新楼B105-JSP动态网页设计课上  完成了java+4399全部课程的学习

阅读全文

2024万能论据

2024/4/17

健康

1.sth contribute to one's health by giving sb some physical exercise.
  某物能有助于身体健康通过给某人一些身体锻炼
2.sth is harmful to/bad for/detrimental to health.
  某物对健康有害
3.sth exert great pressure on sb.
  某物给某人施加重压
4.sb may become more solitary and even suffer from certain mental illness.
  某人可能变得孤僻甚至产生某些心理疾病
5.Exercise can enhance immune function.
  锻炼可以增强免疫力
6.adopt a healthy and meaningful life style.
  采取一个健康和有意义的生活方式
7.release pressure.
  ★减压 

乐趣

1.sth will make ones's life more enjoyable. That is to say, sth can add color to the dull routine of everyday life.
  某物可以使某人的生活更加有乐趣,也就是说,某物为每天枯燥的生活增添了一些色彩
2.sth have become the foucus of one's life and the source of one's happiness and contentment.
  某物已成为某人生活的中心和快乐的来源

能力

1.equip sb with the ability to do sth.
  ★使某人拥有做某事的能力
2.be competent enough to do sth.
  ★有能力做某事
3.tell the useful information from the useless one.
  ★从无用信息中挑出有用信息
4.sth will provide sb with more opportunities to develop one’s interpersonal skills, which may put them in a favorable position in the future job markets.
  某事会使某人有更多机会发展人际交往能力,而这对他(们)未来找工作是非常有好处的
5.By doing sth, sb can not only improve one’s academic studies, but gain many abilities which he will never be able to get from the textbooks.
  通过做某事,某人不仅能够提高他们的专业能力,而且能获得从课本上得不到的能力
6.tap one's potential.
  ★开发潜能
7.sth will help people foster one's independence.
  某物帮助某人培养独立
8.help strengthen one's sense of responsibility.
  ★帮助加强某人的责任感
9.achieve one's goal.
  ★达成某人的目标
10.learn from mistake.
  ★吸取教训
11.surmount difficulities
  ★战胜困难

交流

1.develop and cultivate character and interpersonal skills.
  ★培养性格和人际交往的能力
2.make eye contact.
  ★做眼神交流
3.live only in the virtual world.
  ★仅仅生活在虚拟世界
4.hinder communication.
  ★阻碍沟通
5.The over-dependence on mobile phones can harm the relationship among friends and family.
  过度依赖手机会伤害朋友以及家人的关系
6.At dinner tables, instead of chatting and laughing with each other, many people choose to chat with other friends online.
  在饭桌前,人们不是相互聊天谈笑,而是选择和朋友在网上聊天
7.Many people have become over-dependent on the Internet and neglected face-to-face communication.
  很多人变得太依赖网络并且忽略了面对面交流。
8.The Internet enables more effective communication in some situations, but over-dependence on it actually pulls people apart.
  网络在一些情况下使交流更有效,但是过度依赖它实际是人们更疏远

知识

1.Little by little, our knowledge will be well enriched, and our horizons will be greatly broadened.
  逐渐的,我们的知识会得到增加,我们的视野将会大大的拓宽
2.reduce study load 
  ★减轻学习负担
3.sth can broaden people's minds to the extent which may not be reached previously.
  某物可以扩大人们的视野到一个之前不能到达的程度

方便

1.sth can save sb a great deal of time, so sb would be able to concentrate more time and energy on one's academic work.
  某物能节省大量时间,这会使某人有更多的时间和精力放在学习上
2.sth have brought people great convenience in doing sth.
  某物在人们做某事时带来了巨大的方便
3.There is no denying that sth have greatly enhanced work/study efficiency.
  毋庸置疑,某物很大的提升了工作/学习效率

经济

1.relieve one's families' financial burden to some extent.
  这在一定基础上可以缓解某人家庭的经济负担
2.The welfare and salary have been improved greatly.
  福利和收入得到了巨大的提升
3.Money is so indispensable in people’s lives that without it no material comforts or well-being can be guaranteed.
  钱在生活中必不可少,没有了钱,也就谈不上物质享受和幸福了
4.Money doesn't necessarily ensure happiness or well-being. 
  钱不一定能保证幸福
5.can afford the huge cost of sth.
  ★能够承担某物的高额
6.bring huge economic loss to sb.
  ★给某人带来巨大经济损失

安全

1.sth pose a great threat to one's safety.
  某物对某人的安全造成巨大威胁
2.ensure the safety of sb. 
  ★确保某人的安全
3.sth add to the safety of sth.
  某物增加了某事的安全
4.food safety.
  ★食品安全

环境

1.To balance economic growth with environmental protection is highly important.
  权衡经济发展和环境保护是高度重要的
2.We all should bear in mind that environmental protection is everybody's duty.
  我们都应该记住环境保护使每个人的职责
3.sth help to build harmonious cooperation and effective communication, which helps to create a pleasant working atmosphere.
  某事帮助建立和谐的合作关系和有效的沟通,这将帮助建立一个愉快的工作氛围
阅读全文
头像
Asuna
You are the one who can always get to me even with screen between us.