简历

技术技能

  1. 熟悉 Java 中 IO 流的分类,熟悉 Java 中集合分类以及 ConcurrentHashMap 的底层原理
  2. 熟悉多线程线程池,熟悉 Java 中锁的分类,如乐观锁和悲观锁。
  3. 了解常见的数据结构及算法,如数组、栈、队列、二叉树;冒泡 、插入、选择排序等。
  4. 了解 JVM 内存结构、类加载器和双亲委派机制等,了解常见的垃圾回收器和垃圾回收算法。
  5. 熟悉 MySQL 安装、查询、子查询、内外连接、索引的底层数据存储结构等。
  6. 熟悉事务的隔离级别和数据库锁,了解数据库优化和 sql 优化方案。
  7. 熟悉 Redis 的基本数据类型,主从复制,哨兵模式和集群
  8. 熟悉 Sping + SpringMVC + Mybatis 开发框架,熟悉 Spring 的循环依赖Bean 的生命周期等。
  9. 具备基于 Vue + SpringBoot 的前后端分离开发经验。
  10. 了解 SpringCloud 常用组件的使用,如 Nacos 的服务注册及配置管理OpenFeign 的远程调用等。
  11. 熟悉 HTML、CSS 与 JavaScript,使用过 Ajax 进行前后端数据交互,了解 RESTful API 设计规范。
  12. 了解 Vue 组件、钩子函数等相关知识点,可以使用 Vue 完成基础的页面编写和前后端数据交互。
  13. 了解网络通信框架,了解强缓存协商缓存,了解 TCP 建立连接和断开连接的过程。
  14. 熟悉 Linux 操作系统,掌握常见指令,如文件操作、权限设置等,具备基本的系统管理和维护能力。
  15. 熟练使用 Git、IDEA、ChatGPT、Swagger、Postman 等工具提高开发写作效率。

实习经历

南京攸达网络科技有限公司

2024/07 - 2024/09

工作内容:

负责系统从 CentOS 向 Euler 操作系统的迁移工作,完成数据库与应用服务的平稳过渡与性能检测。

技术要点:

  1. 将部署在 CentOS 系统上的 MySQL、Tomcat 及 Nginx ,完整迁移部署至 Euler 系统中。
  2. 编写 Shell 脚本实现对 CPU、内存占用的实时监控及数据库的定时备份。
  3. 结合 systemctl 与 Python 脚本实现应用的自动化重启与状态维护。
  4. 配置两台数据库服务器实现 MySQL 的互为主从,并结合 Nginx 配置双节点反向代理,实现异常切换。

项目经验

编程导航后台管理

开发环境:JDK8、Mysql5.7、Redis6.2.18、Node16.20.2

项目架构:Maven3.6.3、SpingBoot、Vue3

项目描述:编程导航后台管理(Programming Navigator Admin, PNA)是一种用来管理和控制编程社区的软件系统,帮助工作人员高效管理编程社区内用户、文章及评论。PNA 可以追踪和记录文章以及评论的发布情况,并对评论数进行实时可视化分析。

项目实现:

  1. 登录模块:验证码,管理员注册/登录/退出,管理员头像云存储,管理员信息修改。
  2. 用户模块:新增用户,用户的启动和禁用,查询和修改用户角色,重置用户密码。
  3. 文章/评论模块:新增和修改文章/评论,设置文章/评论状态,评论可视化分析。

技术要点:

  1. 使用 JWT 生成 token,实现登录鉴权,并使用双令牌策略刷新缓存
  2. 使用 Redis 分布式缓存存取评论数据。
  3. 使用 RBAC 进行权限菜单树的开发,实现了用户和权限逻辑分离。
  4. 进行验证码的开发,使用 SHA256 对密码进行盐值加密
  5. 使用动态数据源对数据进行分库存储,使用 Interceptor 进行登录拦截
  6. 使用 AliyunOSS 存取管理员头像,使用 PageHelper 插件实现分页查询
  7. 使用 AOP 进行统一日志和声明式事务管理
  8. 设置 GlobalExceptionHandler 处理异常,并使用 Knife4j 文档进行后端接口测试。
  9. 使用 Vite 脚手架搭建 Vue 前端框架,并使用 ECharts 进行可视化分析。

源码地址:https://gitee.com/make-it-easy/programming-navigator-admin

技术技能

熟悉 Java 中 IO 流的分类,熟悉 Java 中集合分类以及 ConcurrentHashMap 的底层原理

ConcurrentHashMap

ConcurrentHashMap 是线程安全的,它在HashMap的基础上,通过 CAS 或者 synchronized 来保证线程安全的,并且缩小了锁的粒度,查询性能也更高。

JDK 1.7 ConcurrentHashMap采用的是分段锁,即每个Segment是独立的,可以并发访问不同的Segment ,默认是16个Segment ,所以最多有16个线程可以并发执行而JDK 1.8移除了Segment ,缩小了锁的粒度,锁只在链表或红黑树的节点级别上进行。通过CAS进行插入操作,只有在更新链表或红黑树时才使用synchronized,并且只锁住链表或树的头节点,进一步减少了锁的竞争,并发度大大增加。

JDK 1.7 ConcurrentHashMap只使用了数组+链表的结构,而JDK 1.8和HashMap一样引入了红黑树。另外ConcurrentHashMap 在Java 8及之后版本中引入了ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)构造方法,允许设置初始容量、负载因子和并发级别

注意:ConcurrentHashMap中key不能为空

Object类中有哪些常见的方法

  1. toString()
  2. equals(Object obj)
  3. hashCode()
  4. wait()
  5. notify()

熟悉多线程和线程池,熟悉 Java 中锁的分类,如乐观锁和悲观锁

Java中有哪些锁

口诀:克功悲独户,自请分

可重入锁,公平锁,悲观锁,独享锁,互斥锁,自旋锁,轻量级锁,分段锁。

  1. 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程不按照申请锁的顺序来获取锁,有可能会造成饥饿现象。 Java ReentrantLockSynchronized 是非公平锁。
  2. 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 Java ReentrantLockSynchronized 是可重入锁。
  3. 独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。 Java ReentrantLockSynchronized 是独享锁。但是对于Lock的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
  4. 互斥锁/读写锁:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是 ReentrantLock。读写锁在Java中的具体实现就是 ReadWriteLock
  5. 乐观锁/悲观锁:乐观锁认为多线程同时修改共享资源的概率比较低,所以先共享资源,如果出现同时修改的情况,再放弃本次操作。悲观锁认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁。(注意,乐观锁不加锁
  6. 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作
  7. 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对 Synchronized 。Java 5时引入,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  8. 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询(一直申请访问),直到锁被释放。

乐观锁

乐观锁的原理主要基于版本号或时间戳来实现在每次更新数据时,先获取当前数据的版本号或时间戳,然后在更新时比对版本号或时间戳是否一致若一致则更新成功,否则表示数据已被其他线程修改,更新失败。**CAS(Compare-And-Swap)是实现乐观锁的核心算法。**

悲观锁

​ 在Java中,常见的悲观锁实现是使用synchronized关键字或ReentrantLock这些锁能够确保同一时刻只有一个线程可以访问被锁定的代码块或资源,其他线程必须等待锁释放后才能继续执行。

了解常见的数据结构及算法,如数组、栈、队列、二叉树;冒泡 、插入、选择排序等

数据结构

字符和字符串

字符
  1. legnth() - 字符的长度
字符串
  1. legnth - 字符串的长度
  2. s.charAt(i) - 获取字符串s中的第i个字符
  3. str.trim() - 删除字符串首尾空格
  4. substring(int beginIndex, int endIndex) - 截取 [beginIndex, endIndex) 区间的子字符串,左闭右开

数组

数组的特点
  1. 数组的长度不能改变
  2. 数组中的元素的类型必须是同一种数据类型(基本类型/引用类型)
  3. 存入数组中的元素,按照存入的顺序排列

背诵技巧:元长存

数组的动态创建方式和静态创建方式的区别

动态方式:只指定长度,由系统给出初始化值

1
int arr2[] = new int[5];

静态方式:给出初始化值,由系统指定长度

1
int[] arr1 = new int[]{1, 3, 5};
常用方法
基本操作(重要)
  1. push() - 在数组末尾添加一个或多个元素
  2. pop() - 移除并返回数组的最后一个元素
  3. concat() - 合并两个或多个数组,返回新数组
修改和访问方法
  1. indexOf() - 返回元素在数组中首次出现的位置
  2. lastIndexOf() - 返回元素在数组中最后一次出现的位置
  3. includes() - 判断数组是否包含某个元素
迭代方法
  1. forEach() - 对数组每个元素执行函数
  2. filter() - 返回满足条件的元素组成的新数组
  3. find() - 返回第一个满足条件的元素
  4. findIndex() - 返回第一个满足条件的元素的索引

栈和队列

特点

栈是后进先出(LIFO)结构;队列是先进先出(FIFO)结构。

常用方法
栈(重要)
  1. push() - 将元素压入栈顶
  2. pop() - 移除并返回栈顶元素
  3. peek()/top() - 查看栈顶元素但不移除
队列(Qeque)(重要)
  1. enqueue() - 向队尾添加元素
  2. dequeue() - 移除并返回队首元素
  3. peek()/front() - 查看队首元素但不移除
公共方法(重要)
  1. isEmpty() - 检查栈/队列是否为空
  2. size() - 返回栈/队列中元素数量
  3. clear() - 清空栈/队列
双端队列(Deque)
  1. addFront() - 在队首添加元素
  2. addRear() - 在队尾添加元素
  3. removeFront() - 移除队首元素
  4. removeRear() - 移除队尾元素
  5. peekFront() - 查看队首元素
  6. peekRear() - 查看队尾元素

二叉树

前、中、后序遍历方式

前序:根左右,在节点的左侧画点

中序:左根右,在节点的下侧画点

后序:左右根,在节点的右侧画点

前序遍历
中序遍历
后序遍历

算法

排序算法的特性

  1. 自适应排序:数据的输入情况会影响排序算法的时间复杂度,非自适应指的是数据的输入情况不会影响排序算法的时间复杂度

  2. 稳定排序:根据元素某一数值进行排序的排序算法不会改变元素之间的相对位置

  3. 原地排序:不需要额外空间

排序算法共性(人为)

遵从自小到大排序

选择排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package practiceClass.a0421;

public class a01_test {
// 选择排序算法
// 开启一个循环(i),每轮从未排序的区间中选择最小的元素(从i+1开始),将其放在已排序区间(还是左面)的末尾,那么最开始选择的最小就是全局最小
//类似于冒泡排序,需要arr.length - 1 趟,只是减少了交换次数,但是时间复杂度与之相同
// 非自适应,原地,非稳定排序(第一轮最小的元素与第一个的替换,可能第一个和第二个的元素都是相同的,但是第一个被替换到了后面,就是破坏了稳定的排序)
void selectionSort(int[] nums) {
int n = nums.length;
// 外循环:未排序的区间为[i, n-1]
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
// 内层循环从 i + 1 开始,寻找最小值的索引
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[minIndex])
minIndex = j;
}
// 将找到的最小值与当前位置元素交换
int temp = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = temp;
}
}

public static void main(String[] args) {
// 创建实例并调用选择排序方法
// 调用方法时选择new
a01_test sorter = new a01_test();

int[] nums = {1, 2, 5, 6, 7, 2};

sorter.selectionSort(nums);

// 打印排序后的数组
System.out.println("Sorted Array:");
for (int num : nums) {
System.out.print(num + " ");
}
System.out.println();
}
}

冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package practiceClass.a0421;

public class a02_test {
// 冒泡排序算法
// 连续地比较与交换相邻元素实现排序
// 自适应,原地,稳定排序
void bubbleSort(int[] nums) {
int n = nums.length;
// 外循环:未排序的区间为[0, i]
for (int i = n - 1; i > 0; i--) {
boolean flag = false;
// 内循环:将未排序区间[0, i]中的最大元素交换至该区间的最右端
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// 交换元素
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true;
}
}
// 如果本轮没有发生交换,则说明数组已经有序,直接退出循环
if (!flag)
break;
}
}

public static void main(String[] args) {
a02_test sorter = new a02_test();
int[] nums = {1, 2, 5, 6, 7, 2};

sorter.bubbleSort(nums);

// 打印排序后的数组
System.out.println("Sorted Array:");
for (int num : nums) {
System.out.print(num + " ");
}
System.out.println();
}
}

插入排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package practiceClass.a0421;

public class a03_test {
// 插入排序算法
// 自第二个元素,nums[1]开始向前插入到适合的位置
// 自适应,原地,稳定排序
void insertionSort(int[] nums){
int n = nums.length;
for (int i = 1; i < n; i++) {
int base = nums[i]; // 当前要插入的元素
int j = i - 1;
while (j >= 0 && nums[j] > base){
// 因是从左到右逐个进行排序,所以一个元素的后移无伤大雅
nums[j + 1] = nums[j]; // 元素后移
j--;
}
nums[j + 1] = base; // 插入当前元素到正确位置
}
}

public static void main(String[] args) {
a03_test sorter = new a03_test();

int[] nums = {1, 2, 5, 6, 7, 2};

sorter.insertionSort(nums);

// 打印排序后的数组
System.out.println("Sorted Array:");
for (int num : nums) {
System.out.print(num + " ");
}
System.out.println();
}
}

了解 JVM 内存结构、类加载器和双亲委派机制等,了解常见的垃圾回收器和垃圾回收算法

JVM内存相关

熟悉 MySQL 安装、查询、子查询、内外连接、索引的底层数据存储结构

数据库相关

熟悉事务的隔离级别和数据库锁,了解数据库优化和 sql 优化方案

数据库优化

采用动态数据源或者分库分表

数据库相关

熟悉 Redis 的基本数据类型,主从复制,哨兵模式和集群

Redis配置

Redis基础

主从

一主多从

最低: 1 写 3 读

主从,(高可用) 确保 宕机的时候 ,其中一个 上线状态转移成 写

为什么要3个, 因为只有2个没有办法做选举 ,造成 脑裂

修改参数

通用
1
2
3
4
port  7001 ~ 7004 
pidfile
logfile
dir
非通用参数
1
2
# replicaof <masterip> <masterport>
replicaof 127.0.0.1 7002 ~ 7004

哨兵

通过哨兵(工具 )实现主从,而无需自己手动选举

  1. 监控 master 什么时候宕机
  2. 故障转移 当 master 宕机的时候,设置新的 master

配置

从解压的 redis 包中找到 sentinel.conf

1
2
3
4
5
6
daemonize yes
pidfile "/bigdata/sentinel/redis-sentinel.pid"
logfile "/bigdata/sentinel/redis-sentinel.log"
sentinel monitor mymaster 127.0.0.1 7001 1
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000

启动

1
master/bin/redis-sentinel sentinel.conf

集群

cluster

3主 3 从 (最低三从),必须是奇数个服务器 ,防止脑裂

配置

配置文件要修改的

1
2
3
4
port  8001-8006
pidfile
logfile
dir

集群

1
2
cluster-enabled  yes
cluster-config-file nodes(port).conf

每个文件都要修改6项内容

启动

建立集群同时指定 集群的 master 和 slave

1
2
3
4
5
6
7
8
redis1/bin/redis-cli --cluster create \
127.0.0.1:8001 \
127.0.0.1:8002 \
127.0.0.1:8003 \
127.0.0.1:8004 \
127.0.0.1:8005 \
127.0.0.1:8006 \
--cluster-replicas 1

启动的时候 yes

默认前三个是主,后三个是从

熟悉 Sping + SpringMVC + Mybatis 开发框架,熟悉 Spring 的循环依赖,Bean 的生命周期

AOP

概述

面向切面编程(Aspect Oriented Programming),它利用一种称为”横切”的技术,剖解开封装的对象内部,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为切面,降低模块之间的耦合度,有利于未来的操作和维护。

Spring 中**AOP代理由 Spring 的IOC容器负责生成、管理**,其依赖关系也由 IOC 容器负责管理。因此, AOP 代理可以直接使用容器中的其它 bean 实例作为目标。Spring 创建代理的规则为:

  1. 默认使用 JDK 动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
  2. 当需要代理类,而不是代理接口的时候,Spring 会切换为使用 CGLIB代理 ,也可强制使用 CGLIB

纵观 AOP 编程,程序员只需要参与三个部分:

  1. 定义普通业务组件
  2. 定义切入点,一个切入点可能横切多个业务组件
  3. 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作

Spring中AOP的通知类型

  1. @Around(“pointCut()”) :环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  2. @Before(“pointCut()”):前置通知,此注解标注的通知方法在目标方法前被执行
  3. @After(“pointCut()”):后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  4. @AfterReturning(“pointCut()”): 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  5. @AfterThrowing(“pointCut()”): 异常后通知,此注解标注的通知方法发生异常后执行

IOC

什么是 IOC

​ Spring的IOC(inverse of contorl),也就是控制反转,它的核心思想是让对象的创建和依赖关系由容器来控制,不是我们自己new出来的,这样各个组件之间就能保持松散的耦合。IOC是通过依赖注入(DI)来实现的。

为什么需要存在一个容器?

​ 解耦,将对象之间的相互依赖的关系交给IOC容器管理,并让容器完成对象注入,开发者不需要知道一个Service依赖的其 他类,只需要从容器中拿取即可。

Spring Bean

Bean : 被IOC容器所管理的对象俗称Bean,Bean可以通过XML、注解、配置类进行定义。 一般采用注解,如下:

  1. @Component:通用注解
  2. @Repository:持久层 DAO 层
  3. @Service:服务层 Service
  4. @Controller: Spring MVC控制层

怎么注入 Bean?

通常采用注解

  1. @AutoWired按类型装配

Spring的循环依赖

什么是循环依赖

两个或者两个以上的 bean 互相持有对方,最终形成闭环。比如 Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,形成了一个循环依赖关系。这种情况下,如果不处理,会导致 Spring 容器无法完成 Bean 的初始化,从而抛出循环依赖异常。

怎么检测是否存在循环依赖

​ 检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明产生循环依赖了。

如何解决

  • 构造器循环依赖:Spring容器在创建Bean时,如果遇到循环依赖,通常是无法处理的,因为这会导致无限递归创建Bean实例。所以,构造器注入是不支持循环依赖的

  • 字段注入或Setter注入:使用了三级缓存来解决循环依赖问题。

    • 首先,Spring容器会创建一个Bean的原始实例,但此时Bean的属性尚未设置,这个实例被存放在一级缓存中。
    • 当Bean的属性被设置时,如果属性值是其他Bean的引用,Spring会去检查二级缓存,看是否已经有该Bean的引用存在。
    • 如果二级缓存中没有,Spring会尝试创建这个被引用的Bean,并将其放入三级缓存。
    • 最后,当Bean的属性设置完成后,原始的Bean实例会被放入二级缓存,供其他Bean引用
  • 使用@Lazy注解:通过@Lazy注解,可以延迟Bean的加载,直到它被实际使用时才创建,这可以避免一些循环依赖的问题。

Bean的生命周期

背诵技巧:通天(填)A(Aware),B始(使)毁

​ Spring Bean的生命周期,其实就是Spring容器从创建Bean到销毁Bean的整个过程。这里面有几个关键步骤:

  1. 实例化Bean: Spring容器通过构造器或工厂方法(推荐)创建Bean实例
  2. 设置属性:容器会注入Bean的属性,这些属性可能是其他Bean的引用,也可能是简单的配置值。
  3. 检查Aware接口并设置相关依赖:如果Bean实现了BeanNameAwareBeanFactoryAware接口,容器会调用相应的setBeanNamesetBeanFactory方法。
  4. BeanPostProcessor第一次调用:在Bean初始化之前,Spring会调用所有注册的BeanPostProcessorpostProcessBeforeInitialization方法。
  5. 初始化Bean: 如果Bean实现了InitializingBean接口,容器会调用其afterPropertiesSet方法。同时,如果Bean定义了init-method,容器也会调用这个方法。
  6. BeanPostProcessor的第二次调用:在Bean初始化之后,容器会再次调用所有注册的BeanPostProcessorpostProcessAfterInitialization方法。
  7. 使用Bean:此时,Bean已经准备好了,可以被应用程序使用了。
  8. 处理DisposableBeandestroy-method:当容器关闭时,如果Bean实现了DisposableBean接口或者定义了destroy-method,容器会调用其destroy方法。
  9. Bean销毁:最后,Bean被Spring容器销毁,结束了它的生命周期。

具备基于 Vue + SpringBoot 的前后端分离开发经验

了解 SpringCloud 常用组件的使用,如 Nacos 的服务注册及配置管理、OpenFeign 的远程调用等

Nacos 的服务注册及配置管理

​ Nacos (Naming and Configuration Service)是阿里巴巴开源的一个服务注册发现、配置管理的平台,主要用于帮助构建和管理微服务架构。

核心功能

  1. 服务注册与发现:微服务启动时将自己的信息(如IP、端口)注册到Nacos,其他服务可通过Nacos查询可用服务实例
  2. 动态配置管理:集中管理配置信息(如数据库连接参数) ,支持实时更新配置而无需重启服务(热启动)。
  3. 服务健康监测:通过心跳机制监控服务状态,自动剔除故障实例,保证服务可用性。
  4. 流量管理:支持基于权重的路由策略,实现负载均衡和灰度发布

背诵技巧:浮动伏流

需要与Nacos交互的服务

在典型微服务项目中,以下服务需要与Nacos交互:

  1. 服务提供者(Provider) 注册服务:启动时向Nacos注册自己的服务名称、IP和端口。 维持心跳:定期发送心跳包,证明自己处于健康状态。若停止发送,Nacos会将其标记为不可用。
  2. **服务消费者(Consumer) 服务发现:**从Nacos获取目标服务的实例列表,并通过负载均衡策略(如轮询、随机)调用服务。 订阅配置:例如读取数据库连接信息或功能开关,配置变更时自动生效。
  3. 配置管理客户端所有需要动态配置的服务(如Spring Boot应用)会连接Nacos,监听配置变化。例如,修改日志级别后,服务无需重启即可生效。
  4. 网关服务(如Spring Cloud Gateway) 动态路由:从Nacos获取路由规则,例如将“/user/**”的请求转发到用户服务。 服务实例列表:实时更新后端服务的可用实例,避免将请求发送到已宕机的节点。

服务注册流程

接下来,我们把item-service注册到Nacos,步骤如下:

  • 引入依赖
  • 配置Nacos地址
  • 重启
添加依赖

item-servicepom.xml中添加依赖:

1
2
3
4
5
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置Nacos

item-serviceapplication.yml中添加nacos地址配置:

1
2
3
4
5
6
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
启动服务实例

为了测试一个服务多个实例的情况,我们再配置一个item-service的部署实例:

然后配置启动项,注意重命名并且配置新的端口,避免冲突:

重启item-service的两个实例:

访问nacos控制台,可以发现服务注册成功:

点击详情,可以查看到item-service服务的两个实例信息:

服务发现流程

spring-cloud-starter-alibaba-nacos-discovery 依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。

​ 服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。

OpenFeign 的远程调用

需求

OpenFeign 是一个声明式的 Web 服务客户端,它通过注解方式简化了 HTTP 客户端的编写,让我们只需定义接口而无需手动实现 HTTP 请求逻辑。

​ 之前,利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了。因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。

远程调用的关键点就在于四个:

  • 请求方式
  • 请求路径
  • 请求参数
  • 返回值类型

所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。

使用方式

引入依赖

cart-service服务的pom.xml中引入OpenFeign的依赖和loadBalancer依赖:

1
2
3
4
5
6
7
8
9
10
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
启用OpenFeign

接下来,我们在cart-serviceCartApplication启动类上添加注解,启动OpenFeign功能:

编写OpenFeign客户端

cart-service 中,定义一个新的接口,编写Feign客户端:

其中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.hmall.cart.client;

import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient("item-service")
public interface ItemClient {

@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}

这里只需要声明接口,无需实现方法。接口中的几个关键信息:

  • @FeignClient("item-service") :声明服务名称
  • @GetMapping :声明请求方式
  • @GetMapping("/items") :声明请求路径
  • @RequestParam("ids") Collection<Long> ids :声明请求参数
  • List<ItemDTO> :返回值类型

​ 有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>

我们只需要直接调用这个方法,即可实现远程调用了。

使用FeignClient

最后,我们在cart-servicecom.hmall.cart.service.impl.CartServiceImpl中改造代码,直接调用ItemClient的方法:

feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作。

而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册。

熟悉 HTML、CSS 与 JavaScript,使用过 Ajax 进行前后端数据交互,了解 RESTful API 设计规范

Input标签中type属性有哪些取值,分别表示什么

type取值 描述
text 文本框
password 密码框
radio 单选框
checkbox 复选框
file 上传图像
hidden 隐藏字段
submit 提交按钮
reset 重置按钮
button 普通按钮
image 图片按钮

背诵技巧:文秘(密)担(单)负(复)上瘾(隐)重提谱(普)图

CSS中选择器的分类

基础选择器

  1. 标签选择器
  2. 选择器
  3. id选择器
  4. 通配符选择器

关系选择器

  1. 交集选择器:没有符号
  2. 并集选择器:以逗号隔开
  3. 亲子选择器:以>隔开
  4. 后代选择器:以空格隔开

属性选择器

两种情况,一种是方括号中属性名+属性值,一种是方括号中只有属性名

伪类选择器

伪类通过冒号来定义元素的状态

背诵技巧:机(基)关属委(伪)

不算技巧的技巧:标类i通,交并亲后

JS中的基础数据类型

  1. number类型:整数、浮点数、NaN、infinity(正负无穷大)
  2. boolean类型:true 、false
  3. string类型:有三种表示形式,单引号、双引号、反引号,在js中没有字符的概念
  4. null类型
  5. undefined类型:既是一个数据类型,也是一个值;undefined作用是用来提醒我们该变量没有赋值就被使用了

背诵技巧:对应java中常量的分类来背。

不算技巧的技巧:nb s nu

JS中==和===的区别

  1. ==:只比较值,不比较类型
  2. ===:既比较值,也比较类型

JS中数组和Java中数组的区别

  1. js中数组的长度是可变的,而java中的数组长度不能改变
  2. js中数组可以存放多种类型的数据,而java中的数组只能存放同一种类型的元素

背诵技巧:常(长)数

ES6新特性有哪些

  1. let关键字,防止变量被重复声明,定义的变量具有块级作用域,必须先声明再使用
  2. const关键字,定义常量,声明时就要赋值,赋值一次之后,不能再次赋值
  3. 字符串支持反引号
  4. 箭头函数(java中的lambda)+ 可变参数

背诵技巧:class —- c(const)l(let)a(arrow)s(string)

JS中innerHtml和innerText的区别

  • innertext:只能获取文本信息,不能获取标签信息
  • innerhtml:既可以获取文本信息,也能获取标签信息

Ajax

概念

​ Ajax即Asynchronous Javascript And XML(异步JavaScript和XML)在 2005年被Jesse James Garrett提出的新术语,用来描述一种使用现有技术集合的‘新’方法,包括: HTML 或 XHTML, CSS, JavaScript, DOM, XML, XSLT, 以及最重要的XMLHttpRequest。

使用Ajax技术,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面,这使得程序能够更快地回应用户的操作。

Ajax的实现方式

原生Ajax
  • 第一步:创建XMLHttpRequest对象,用于和服务器交换数据,也是原生Ajax请求的核心对象,提供了各种方法。代码如下:

    1
    2
    //1. 创建XMLHttpRequest 
    var xmlHttpRequest = new XMLHttpRequest();

    第二步:调用对象的open()方法设置请求的参数信息,例如请求地址,请求方式。然后调用send()方法向服务器发送请求,代码如下:

    1
    2
    3
    //2. 发送异步请求
    xmlHttpRequest.open('GET','http://yapi.smart-xwork.cn/mock/169327/emp/list');
    xmlHttpRequest.send();//发送请求

    第三步:我们通过绑定事件的方式,来获取服务器响应的数据。

    1
    2
    3
    4
    5
    6
    7
    //3. 获取服务响应数据
    xmlHttpRequest.onreadystatechange = function(){
    //此处判断 4表示浏览器已经完全接受到Ajax请求得到的响应, 200表示这是一个正确的Http请求,没有错误
    if(xmlHttpRequest.readyState == 4 && xmlHttpRequest.status == 200){
    document.getElementById('div1').innerHTML = xmlHttpRequest.responseText;
    }
    }

Axios

Axios是对原生的AJAX进行封装,简化书写

使用方式

Axios的使用比较简单,主要分为2步:

  • 引入Axios文件

    1
    <script src="js/axios-0.18.0.js"></script>
  • 使用Axios发送请求,并获取响应结果,官方提供的api很多,此处给出2种,如下

    • 发送 get 请求

      1
      2
      3
      4
      5
      6
      axios({
      method:"get",
      url:"http://localhost:8080/ajax-demo1/aJAXDemo1?username=zhangsan"
      }).then(function (resp){
      alert(resp.data);
      })
    • 发送 post 请求

      1
      2
      3
      4
      5
      6
      7
      axios({
      method:"post",
      url:"http://localhost:8080/ajax-demo1/aJAXDemo1",
      data:"username=zhangsan"
      }).then(function (resp){
      alert(resp.data);
      });

    axios()是用来发送异步请求的,小括号中使用 js的JSON对象传递请求相关的参数:

    • method属性:用来设置请求方式的。取值为 get 或者 post。
    • url属性:用来书写请求的资源路径。如果是 get 请求,需要将请求参数拼接到路径的后面,格式为: url?参数名=参数值&参数名2=参数值2。
    • data属性:作为请求体被发送的数据。也就是说如果是 post 请求的话,数据需要作为 data 属性的值。

    then() 需要传递一个匿名函数。我们将 then()中传递的匿名函数称为 回调函数,意思是该匿名函数在发送请求时不会被调用,而是在成功响应后调用的函数。而该回调函数中的 resp 参数是对响应的数据进行封装的对象,通过 resp.data 可以获取到响应的数据。

Restful风格代码

Restful风格的接口是一种基于资源的设计风格,用于构建面向Web的API。 REST (Representational State Transfer)是一种无状态的架构风格,它以 HTTP 协议为基础,通过定义资源和标准的操作方法来组织接口,使得客户端和服务器之间的交互更加简单、清晰和高效。

​ 实际上Restful就是要求我们不要在URL上表现出动作,而是用HTTP动词代表动作,URL上只做资源。比如获取一个 user。

非Restful : GET /getUserByld?userld=1

Restful : GET /users/1

​ 对应到SpringMVC代码就是 urlaa不写什么get之类的动词,而是通过HTTP请求(如GET,POST,PUT,DELETE)访问服务端资源。

了解 Vue 组件、钩子函数等相关知识点,可以使用 Vue 完成基础的页面编写和前后端数据交互

Vue指令

总括

  • template相当于html
  • script相当于javascript
  • style相当于css

背诵技巧:A组爽(双)文是(事)薯(属)条

v-bind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<div>
<!-- 动态图片路径 -->
<img :src="user.avatar" :alt="user.name">

<!-- 动态类名 -->
<button :class="['btn', { 'btn-active': isActive }]">Click me</button>

<!-- 动态样式 -->
<div :style="{
backgroundColor: bgColor,
width: width + 'px',
height: height + 'px'
}"></div>

<!-- 传递props给子组件 -->
<user-card :user="currentUser" :is-following="followStatus"></user-card>

<!-- 动态表单绑定 -->
<input :value="inputValue" @input="inputValue = $event.target.value">
</div>
</template>

<script>
export default {
data() {
return {
user: {
name: 'John Doe',
avatar: '/path/to/avatar.jpg'
},
isActive: true,
bgColor: '#42b983',
width: 100,
height: 100,
currentUser: { /* ... */ },
followStatus: false,
inputValue: ''
}
}
}
</script>
文本插值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<template>
<div>
<!-- 1. 显示简单数据 -->
<h1>{{ title }}</h1>

<!-- 2. 显示计算后的数据 -->
<p>总价: ¥{{ quantity * price }}</p>

<!-- 3. 显示格式化后的日期 -->
<p>当前时间: {{ formatDate(currentDate) }}</p>

<!-- 4. 显示计算属性 -->
<p>反转消息: {{ reversedMessage }}</p>

<!-- 5. 条件显示 -->
<div v-if="isLoggedIn">
欢迎回来, {{ user.name }}!
您的会员等级是: {{ user.level.toUpperCase() }}
</div>

<!-- 6. 在属性中使用(不推荐,应使用v-bind) -->
<span title="{{ tooltip }}">悬停查看提示</span>

<!-- 7. 显示数组或对象内容 -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} - 库存: {{ item.stock }}
</li>
</ul>
</div>
</template>

<script>
export default {
data() {
return {
title: '产品详情',
quantity: 5,
price: 29.9,
currentDate: new Date(),
message: 'Hello Vue!',
isLoggedIn: true,
user: {
name: '张三',
level: 'gold'
},
tooltip: '这是一个提示信息',
items: [
{ id: 1, name: '商品A', stock: 10 },
{ id: 2, name: '商品B', stock: 5 }
]
}
},
computed: {
reversedMessage() {
return this.message.split('').reverse().join('')
}
},
methods: {
formatDate(date) {
return date.toLocaleDateString()
}
}
}
</script>

指令集

指令 作用
v-bind(:) 为HTML标签绑定属性值,如设置 href , css样式等
v-model 在表单元素上创建双向数据绑定
v-on(@) 为HTML标签绑定事件
v-if 条件性的渲染某元素,判定为true时渲染,否则不渲染
v-else
v-else-if
v-show 根据条件展示某元素,区别在于切换的是display属性的值
v-for 列表渲染,遍历容器的元素或者对象的属性
v-slot(#) 插槽

Vue钩子函数

  1. computed:
  2. created ()
  3. mounted ()
  4. methods:

Vue生命周期(钩子函数)

了解网络通信框架,了解强缓存和协商缓存,了解 TCP 建立连接和断开连接的过程

熟悉 Linux 操作系统,掌握常见指令,如文件操作、权限设置等,具备基本的系统管理和维护能力

熟练使用 Git、IDEA、ChatGPT、Swagger、Postman 等工具提高开发写作效率

实习经历

完整迁移部署至 Euler 系统中

数据库的定时备份

编辑备份 shell 脚本

vim mysql_back.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/bin/bash

#指定连接数据库信息(用户名、密码、连接地址、端口、安装目录)
DB_USER="root"
DB_PWD="password"
DB_IP="host"
DB_PORT="3306"
#是指mysqldump命令所在目录
DB_DIR="/usr/local/mysql"

#获取系统当前时间并格式化为:20210729
BAK_DATE=`date +%Y%m%d`

#指定备份文件保存的天数
BAK_DAY=7
#指定备份的数据库,可以指定多个中间用空格隔开,或者不指定则默认全部备份
BAK_DATABASES=("")
#指定备份路径
BAK_PATH="/data/mysql_back"

#创建备份目录
mkdir ${BAK_PATH}/$BAK_DATE

#开始执行备份
echo "------- $(date +%F_%T) Start MySQL database backup-------- " >>${BAK_PATH}/back.log
#循环遍历
for database in "${BAK_DATABASES[@]}"
do
${DB_DIR}/bin/mysqldump -u${DB_USER} -p${DB_PWD} --host=${DB_IP} --port=${DB_PORT} --databases $database > ${BAK_PATH}/${BAK_DATE}/${database}.sql
done

#创建压缩文件
cd ${BAK_PATH}
tar -zcPf db_backup_${BAK_DATE}.tar.gz $BAK_DATE

#删除备份目录
mv ${BAK_PATH}/$BAK_DATE /tmp1

#遍历备份目录下的文件
LIST=$(ls ${BAK_PATH}/db_backup_*)

#获取截止时间,将早于改时间的文件删除

SECONDS=$(date -d "$(date +%F) - ${BAK_DAY} days" +%s)

for index in ${LIST}
do
#获取文件名并格式化,获取时间,如20210729
timeString=$(echo ${index} | egrep -o "?[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]")
if [ -n "$timeString" ]
then
indexDate=${timeString//./-}
indexSecond=$( date -d ${indexDate} +%s )
#与当前时间做比较,把早于7天的文件删除
if [ $(( $SECOND - $indexDate )) -gt 0 ]
then
rm -f $index
echo "-------deleted old file $index -------" >> ${BAK_PATH}/back.log
fi
fi
done

echo "-------$(date +%F_%T) Stop MySQL database backup-------- " >>${BAK_PATH}/back.log


查看日志文件

cat /opt/mysql_back/back.log

1
2
3
cat /opt/mysql_back/back.log 
------- 2021-07-29_18:25:09 Start MySQL database backup--------
-------2021-07-29_18:25:09 Stop MySQL database backup--------

解压压缩包查看备份脚本

1
tar -zxvf db_backup_20210729.tar.gz

设置定时任务以实现定时备份

crontab -e

1
2
3
crontab -e
#每天凌晨3点执行备份,避免影响业务使用,备份时会锁表
0 3 * * * sh /root/mysql_back.sh

自动化重启与状态维护

状态维护

介绍

​ MySQL 通常通过系统包管理器(yum)安装,所以会自动注册到 systemctl 服务管理中,但是如果是通过下载压缩包手动安装,就需要手动注册到 systemctl 服务管理中。

​ 对 MySQL 和 Tomcat 都进行注册,以下只介绍 MySQL 的注册流程

创建 systemd 服务文件

1
sudo nano /etc/systemd/system/mysqld.service 

添加服务配置内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[Unit]
Description=MySQL Community Server
After=network.target
After=syslog.target

[Service]
Type=forking
User=mysql
Group=mysql

# 根据你的实际安装路径修改
Basedir=/usr/local/mysql
Datadir=/usr/local/mysql/data

# 指定PID文件位置(需与my.cnf中配置一致)
PIDFile=/usr/local/mysql/data/mysqld.pid

# 启动命令
ExecStart=/usr/local/mysql/bin/mysqld --daemonize --basedir=$Basedir --datadir=$Datadir --pid-file=$PIDFile

# 停止命令
ExecStop=/usr/local/mysql/bin/mysqladmin -uroot -p shutdown

# 重启命令
ExecRestart=/usr/local/mysql/bin/mysqladmin -uroot -p shutdown && /usr/local/mysql/bin/mysqld --daemonize --basedir=$Basedir --datadir=$Datadir --pid-file=$PIDFile

# 设置重启策略
Restart=on-failure
RestartSec=5s

PrivateTmp=true

[Install]
WantedBy=multi-user.target

设置权限并重载 systemd

1
2
3
ssudo chmod 644 
/etc/systemd/system/mysqld.service
sudo systemctl daemon-reload

修改数据目录权限

1
sudo chown -R mysql:mysql /usr/local/mysql/data

启动 MySQL 服务

1
sudo systemctl start mysql

设置开机自启

1
sudo systemctl enable mysqld

验证服务状态

1
sudo systemctl status mysqld

自动化重启

介绍

使用python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import pymysql
import subprocess
import time

# 配置信息
MYSQL_CONFIG = {
'host': 'localhost',
'user': 'root',
'password': 'your_mysql_password',
'database': 'test_db',
'port': 3306
}

# 测试数据(多条插入)
TEST_DATA = [
("Alice", "alice@example.com"),
("Bob", "bob@example.com"),
("Charlie", "charlie@example.com")
]

def insert_test_data():
"""向 MySQL 插入测试数据"""
try:
connection = pymysql.connect(**MYSQL_CONFIG)
cursor = connection.cursor()

# 示例:插入数据到 users 表
sql = "INSERT INTO users (name, email) VALUES (%s, %s)"
cursor.executemany(sql, TEST_DATA)
connection.commit()
print(f"成功插入 {len(TEST_DATA)} 条测试数据。")

except Exception as e:
print(f"插入数据失败: {e}")
finally:
if connection:
connection.close()

def restart_mysql():
"""重启 MySQL 服务(Linux 系统)"""
try:
subprocess.run(["sudo", "systemctl", "restart", "mysql"], check=True)
print("MySQL 重启成功,等待 5 秒...")
time.sleep(5) # 等待服务完全启动
except subprocess.CalledProcessError as e:
print(f"重启 MySQL 失败: {e}")

def restart_tomcat():
"""重启 Tomcat 服务(假设 Tomcat 安装在 /opt/tomcat)"""
try:
subprocess.run(["/opt/tomcat/bin/shutdown.sh"], check=True)
time.sleep(3) # 等待关闭
subprocess.run(["/opt/tomcat/bin/startup.sh"], check=True)
print("Tomcat 重启成功,等待 10 秒...")
time.sleep(10) # 等待应用启动
except Exception as e:
print(f"重启 Tomcat 失败: {e}")

if __name__ == "__main__":
# 1. 插入测试数据
insert_test_data()

# 2. 重启 MySQL
restart_mysql()

# 3. 重启 Tomcat
restart_tomcat()

print("流程执行完毕!")

互为主从

Mysql双机和Nginx反向代理

项目经验

使用 JWT 生成 token,实现登录鉴权,并使用双令牌策略刷新缓存

JWT

JWT介绍

JWT全称:JSON Web Token,定义了一种简洁的、自包含的格式,用于在通信双方间以 json 数据格式安全的传输信息。

简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。

自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。

JWT Token通常由三个部分组成,它们之间用“.” 分隔:

  1. Header (头部) :用于存储有关如何计算JWT签名的信息,如对象类型(type,通常为JWT) 、签名算法(alg,如HS256、 RSA等)。这些信息会经过Base64Url编码后形成JWT的第一部分
  2. Payload (有效载荷) :也称为”body” ,用于存储用户信息,如用户ID 、Email、角色(role)、权限信息(permission) 、签发时间 、过期时间(exp)等。这些信息同样会经过Base64Url编码后形成JWT的第二部分
  3. Signature (签名) :使用Header中指定的算法,将编码后的Header、Payload以及一个密钥(secret)进行加密,生成最终的签名。这个签名用于验证信息在传输过程中是否被篡改,并且在使用私钥签名令牌的情况下,它还可以验证JWT的发送者是否正确。签名会作为JWT的第三部分。

JWT Token的应用

场景:登录认证。

  1. 登录成功后,生成令牌
  2. 后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理

具体使用

1
2
3
4
5
6
7
8
9
// JwtUtils.java类中
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "itheima")//签名算法
.setClaims(claims) //自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为1h
.compact(); // 字符串类型的返回值

// LoginController.java类中
String jwt = JwtUtils.generateJwt(claims); //jwt包含了当前登录的员工信息

Redis缓存Token

流程

  1. 导入Spring Data Redis 的 maven 坐标
  2. 设置Redis数据源
  3. 编写配置类,创建RedisTemplate对象
  4. 通过RedisTemplate对象操作Redis

具体

1
2
3
4
5
6
7
8
9
// LoginController.java类中
// 自动注入
@Autowired
private StringRedisTemplate redisTemplate;

//jwt包含了当前登录的员工信息
String jwt = JwtUtils.generateJwt(claims);
// 将token存入redis
redisTemplate.opsForValue().set(jwt, 30, TimeUnit.MINUTES);

双令牌刷新策略

双令牌相比单令牌的优点

  1. 双令牌中 refresh_token 一天只需要一次
  2. 单令牌每次都要使用,所以暴露的概率更大

使用 Redis 分布式缓存存取评论数据

使用 RBAC 进行权限菜单树的开发,实现了管理员和权限逻辑分离

进行验证码的开发,使用 SHA256 对密码进行盐值加密

验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebServlet(name = "ServletCaptcha", value = "/captcha")
public class ServletCaptcha extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//使用hutool工具包生成查看用户
GifCaptcha captcha = CaptchaUtil.createGifCaptcha(150, 45, 4);
//获取验证码的值
String code = captcha.getCode();
//将验证码保存到sessioon
request.getSession().setAttribute("code",code);
//将验证码写回前端页面
captcha.write(response.getOutputStream());
}
}

密码盐值加密(需修改)

引入 MD5Util.java 工具类

  1. MessageDigest.getInstance 生成加密计算摘要md
  2. 根据 md.digest() 计算得到8位字符串
  3. 通过 BigInteger() 返回十六进制字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.muyi.utils;

import java.math.BigInteger;
import java.security.MessageDigest;

public class MD5Util {
//用户
public static final String SALT = "yyds";

public static String getMD5String(String str) {
try {
// 生成一个MD5加密计算摘要
MessageDigest md = MessageDigest.getInstance("MD5");
// 计算md5函数
byte[] digest = md.digest(str.getBytes());
// digest()最后确定返回md5 hash值,返回值为8位字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
// BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
// 一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方)
return new BigInteger(1, digest).toString(16);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

AccountController.java类中

1
2
3
4
5
6
7
8
9
10
11
12
@ApiOperation(value = "新增管理员")
@PostMapping
public Result add(@RequestBody Account Account){
log.info("新增管理员: {}" , Account);
//调用service新增管理员
// 密码加密
String s1 = MD5Util.getMD5String(Account.getPassword());
String pwdJM = MD5Util.getMD5String(s1 + MD5Util.SALT);
Account.setPassword(pwdJM);
AccountService.add(Account);
return Result.success();
}

使用动态数据源对数据进行分库存储,使用 Interceptor 进行登录拦截

数据库设计

分表分库 的优点

  1. 减轻数据库的压力
  2. 业务分离

动态数据源引入 pom.xml

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>

配置文件配置

可以在 springboot 中配置 master , base , order ,…

使用

在 mapper 层加注解 @DS

原理

AOP ,动态代理 ,

核心类

DynamicDataSourceAnnotationInterceptor implements MethodInterceptor

动态数据源注解拦截 实现 方法拦截

统一异常处理和拦截器

流程图

实现HandlerInterceptor

位置

interceptor目录下

具体流程

  • 当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。

  • Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。

  • 当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行preHandle()方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。

  • 在controller当中的方法执行完毕之后,再回过来执行postHandle()这个方法以及afterCompletion() 方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。

简略流程

  1. preHandle : 调用controller之前
  2. postHandle:调用controller之后
  3. afterCompletion:视图渲染之后(这里的视图渲染指的是服务器将动态资源转化为静态资源)

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");

return true; //true表示放行
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ... ");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion .... ");
}
}

配置拦截器

位置

config目录下

简略流程

  1. 实现 WebMvcConfigurer
  2. 重写 addInterceptors

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 拦截器关键代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

// 拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/login"); //不拦截登录请求
}
}

使用 AliyunOSS 存取管理员头像,使用 PageHelper 插件实现分页查询

AliyunOSS

开通OSS服务

  1. 登录阿里云控制台,开通OSS服务
  2. 创建 Bucket(存储空间),选择合适的地域和存储类型 endpoint

获取访问密钥

  1. 在阿里云RAM访问控制中创建子账号
  2. 为子账号分配OSS读写权限
  3. 获取AccessKey IDAccessKey Secret

后端配置

在 utils 工具包下导入AliOSSUtils类,并在该类下创建 upload 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String upload(MultipartFile file) throws IOException {
//获取阿里云OSS参数
// String endpoint = aliOSSProperties.getEndpoint();
// String accessKeyId = aliOSSProperties.getAccessKeyId();
// String accessKeySecret = aliOSSProperties.getAccessKeySecret();
// String bucketName = aliOSSProperties.getBucketName();


String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
String accessKeyId = "LTAI5tBhgTYGfP2be8vKf3Du";
String accessKeySecret = "oO1d4rMsMnANw2H3BWk24JqSf9Azr3";
String bucketName = "web-tlias-ycl-test";
/**
* 实现上传图片到OSS,后续内容省略
*/

创建uploadController

1
2
3
4
5
6
7
8
9
10
@PostMapping("/upload")
public Result upload(MultipartFile image) throws IOException {
log.info("文件上传, 文件名: {}", image.getOriginalFilename());

// 调用阿里云OSS工具类进行文件上传
String url = aliOSSUtils.upload(image);
// 将图片上传完成后的url返回,用于浏览器回显展示
log.info("文件上传完成,文件访问的url: {}", url);

return Result.success(url);

使用 AOP 进行统一日志和声明式事务管理

AOP 进行统一日志

需求

统计各个业务层方法执行耗时。

实现步骤

  1. 导入依赖:在 pom.xml 中导入 AOP 的依赖
  2. 编写 AOP 程序:针对于特定方法根据业务需要进行编程

为演示方便,可以自建新项目或导入提供的springboot-aop-quickstart项目工程

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>

AOP程序:TimeAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {

// 定义Around环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();

//执行原始方法
Object result = pjp.proceed();

//记录方法执行结束时间
long end = System.currentTimeMillis();

//计算方法执行耗时
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);

return result;
}
}

声明式事务管理 @Transactional

作用

​ 在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。

书写位置

  1. 方法:当前方法交给spring进行事务管理
  2. 类:当前类中所有的方法都交由spring进行事务管理
  3. 接口:接口下所有的实现类当中所有的方法都交给spring 进行事务管理

查看事务相关日志

在application.yml配置文件中开启事务管理日志

1
2
3
4
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug

重要参数

rollbackFor

默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;

@Autowired
private EmpMapper empMapper;


@Override
@Transactional(rollbackFor=Exception.class)
public void delete(Integer id){
//根据部门id删除部门信息
deptMapper.deleteById(id);

//模拟:异常发生
int num = id/0;

//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}
propagation

​ propagation是用来配置事务的传播行为的传播行为是指当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制

属性值 含义
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 【记录日志时使用】需要新事务,无论有无,总是创建新事务
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class DeptLogServiceImpl implements DeptLogService {

@Autowired
private DeptLogMapper deptLogMapper;

// 事务传播行为:不论是否有事务,都新建事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}

业务实现类:DeptServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Slf4j
@Service
//@Transactional //当前业务实现类中的所有的方法,都添加了spring事务管理机制
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;

@Autowired
private EmpMapper empMapper;

@Autowired
private DeptLogService deptLogService;


//根据部门id,删除部门信息及部门下的所有员工
@Override
@Log
@Transactional(rollbackFor = Exception.class)
public void delete(Integer id) throws Exception {
try {
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常
if(true){
throw new Exception("出现异常了~~~");
}
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}finally {
//不论是否有异常,最终都要执行的代码:记录日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
//调用其他业务类中的方法
deptLogService.insert(deptLog);
}
}

//省略其他代码...
}

设置 GlobalExceptionHandler 处理异常,并使用 Knife4j 文档进行后端接口测试

核心注解

  1. @ControllerAdvice :异常控制器,推荐使用@RestControllerAdvice(整体代码更简洁)
  2. @ExceptionHandler: 声明异常类型

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// @ControllerAdvice
// public class GlobalExceptionHandler {
//
// // 需要手动添加 @ResponseBody 才能返回 JSON
// @ExceptionHandler(Exception.class)
// @ResponseBody
// public Result ex(Exception ex){
// ex.printStackTrace();
// return Result.error("对不起,操作失败,请联系管理员");
// }
// }

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(Exception.class)//捕获所有异常
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}

引入配置

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

配置包下配置 Swagger 文档生成器

定义API文档的基本信息和扫描范围,使系统能够自动识别并生成指定包下控制器的API接口文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itheima.config;

import org.springdoc.core.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

// 配置 API 文档
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public-api")
.pathsToMatch("/**") // 定义需要文档化的接口路径
.build();
}
}

配置包下配置 Knife4j 文档生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.itheima.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {

@Bean(value = "defaultApi2")
public Docket defaultApi2() {
Docket docket=new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
//.title("swagger-bootstrap-ui-demo RESTful APIs")
.description("# 教学辅助系统 RESTful APIs")
.termsOfServiceUrl("http://www.xx.com/")
.contact("13017536058@163.com")
.version("1.0")
.build())
//分组名称
.groupName("2.X版本")
.select()
//这里指定Controller扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.itheima.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}

}

修改配置类

1
2
3
4
5
6
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui/index.html # 自定义 Swagger UI 的路径
enabled: true # 启用 Swagger UI

文档页面

  1. Swagger 原生界面: http://localhost:8080/swagger-ui.html
  2. Knife4j 增强界面: http://localhost:8080/doc.html

中文解释注解

  1. @Api: 用在类上,说明该类的作用
  2. @ApiOperation: 用在方法上,说明方法的作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.muyi.controller;

import com.muyi.pojo.entity.Result;
import com.muyi.pojo.entity.Tag;
import com.muyi.service.TagService;
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;

@Api(tags = "标签模块")
@Slf4j
@CrossOrigin
@RequestMapping("/tag")
@RestController
public class TagController {
@Autowired
private TagService tagService;

/**
* 查询标签数据
* @return
*/
@ApiOperation(value = "查询全部标签")
@GetMapping
public Result list(){
log.info("查询全部标签数据");
//调用service查询标签数据
List<Tag> tagList = tagService.list();
return Result.success(tagList);
}
}

使用 Vite 脚手架搭建 Vue 前端框架,并使用 ECharts 进行可视化分析

创建Vue3项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用 npm 安装 pnpm
npm install -g pnpm
pnpm -v
# 应显示版本号,如 8.x.x

mkdir my-frontend-project
cd my-frontend-project

pnpm init

pnpm create vite

# 安装必要依赖,可直接使用pnpm install
# 核心库
pnpm add vue vue-router pinia
# 开发工具
pnpm add -D vite @vitejs/plugin-vue
# 常用工具库
pnpm add axios lodash-es
# UI 库(以 Element Plus 为例)
pnpm add element-plus @element-plus/icons-vue
# CSS 预处理器
pnpm add -D sass

可视化分析(需修改)

安装echarts

1
pnpm add echarts

全局引入 echarts.min.js

1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from './App.vue'
import * as echarts from 'echarts'

const app = createApp(App)
app.config.globalProperties.$echarts = echarts
app.mount('#app')

使用案例

核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
// 更新图表
this.myChart.setOption({
// 数据项
series: [
{
// data: [
// { value: 1048, name: '球鞋' },
// { value: 735, name: '防晒霜' }
// ]
data: this.list.map(item => ({ value: item.price, name: item.name}))
}
]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- CSS only -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
/>
<style>
.red {
color: red!important;
}
.search {
width: 300px;
margin: 20px 0;
}
.my-form {
display: flex;
margin: 20px 0;
}
.my-form input {
flex: 1;
margin-right: 20px;
}
.table > :not(:first-child) {
border-top: none;
}
.contain {
display: flex;
padding: 10px;
}
.list-box {
flex: 1;
padding: 0 30px;
}
.list-box a {
text-decoration: none;
}
.echarts-box {
width: 600px;
height: 400px;
padding: 30px;
margin: 0 auto;
border: 1px solid #ccc;
}
tfoot {
font-weight: bold;
}
@media screen and (max-width: 1000px) {
.contain {
flex-wrap: wrap;
}
.list-box {
width: 100%;
}
.echarts-box {
margin-top: 30px;
}
}
</style>
</head>
<body>
<div id="app">
<div class="contain">
<!-- 左侧列表 -->
<div class="list-box">

<!-- 添加资产 -->
<form class="my-form">
<input v-model.trim="name" type="text" class="form-control" placeholder="消费名称" />
<input v-model.number="price" type="text" class="form-control" placeholder="消费价格" />
<button @click="add" type="button" class="btn btn-primary">添加账单</button>
</form>

<table class="table table-hover">
<thead>
<tr>
<th>编号</th>
<th>消费名称</th>
<th>消费价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td :class="{ red: item.price > 500 }">{{ item.price.toFixed(2) }}</td>
<td><a @click="del(item.id)" href="javascript:;">删除</a></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">消费总计: {{ totalPrice.toFixed(2) }}</td>
</tr>
</tfoot>
</table>
</div>

<!-- 右侧图表 -->
<div class="echarts-box" id="main"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
* 接口文档地址:
* https://www.apifox.cn/apidoc/shared-24459455-ebb1-4fdc-8df8-0aff8dc317a8/api-53371058
*
* 功能需求:
* 1. 基本渲染
* (1) 立刻发送请求获取数据 created
* (2) 拿到数据,存到data的响应式数据中
* (3) 结合数据,进行渲染 v-for
* (4) 消费统计 => 计算属性
* 2. 添加功能
* (1) 收集表单数据 v-model
* (2) 给添加按钮注册点击事件,发送添加请求
* (3) 需要重新渲染
* 3. 删除功能
* (1) 注册点击事件,传参传 id
* (2) 根据 id 发送删除请求
* (3) 需要重新渲染
* 4. 饼图渲染
* (1) 初始化一个饼图 echarts.init(dom) mounted钩子实现
* (2) 根据数据实时更新饼图 echarts.setOption({ ... })
*/
const app = new Vue({
el: '#app',
data: {
list: [],
name: '',
price: ''
},
computed: {
totalPrice () {
return this.list.reduce((sum, item) => sum + item.price, 0)
}
},
created () {
// const res = await axios.get('https://applet-base-api-t.itheima.net/bill', {
// params: {
// creator: '小黑'
// }
// })
// this.list = res.data.data

this.getList()
},
mounted () {
this.myChart = echarts.init(document.querySelector('#main'))
this.myChart.setOption({
// 大标题
title: {
text: '消费账单列表',
left: 'center'
},
// 提示框
tooltip: {
trigger: 'item'
},
// 图例
legend: {
orient: 'vertical',
left: 'left'
},
// 数据项
series: [
{
name: '消费账单',
type: 'pie',
radius: '50%', // 半径
data: [
// { value: 1048, name: '球鞋' },
// { value: 735, name: '防晒霜' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
},

methods: {
async getList () {
const res = await axios.get('https://applet-base-api-t.itheima.net/bill', {
params: {
creator: '小黑'
}
})
this.list = res.data.data

// 更新图表
this.myChart.setOption({
// 数据项
series: [
{
// data: [
// { value: 1048, name: '球鞋' },
// { value: 735, name: '防晒霜' }
// ]
data: this.list.map(item => ({ value: item.price, name: item.name}))
}
]
})
},
async add () {
if (!this.name) {
alert('请输入消费名称')
return
}
if (typeof this.price !== 'number') {
alert('请输入正确的消费价格')
return
}

// 发送添加请求
const res = await axios.post('https://applet-base-api-t.itheima.net/bill', {
creator: '小黑',
name: this.name,
price: this.price
})
// 重新渲染一次
this.getList()

this.name = ''
this.price = ''
},
async del (id) {
// 根据 id 发送删除请求
const res = await axios.delete(`https://applet-base-api-t.itheima.net/bill/${id}`)
// 重新渲染
this.getList()
}
}
})
</script>
</body>
</html>