抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

Hello world!

统计分析后台(后端+前端)

环境搭建

老样子

生成统计数据(后端)

注意,要查询什么数据要在对应模块中查询,然后在统计模块远程调用获取

被调用方controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/educenter/member")
@CrossOrigin
public class UcenterMemberController {
@Autowired
private UcenterMemberService memberService;
// 查询特定天的注册人数
@GetMapping("countRegister/{day}")
public Integer countRegister(@PathVariable String day) {
Integer count = memberService.countRegister(day);
return count;
}
}

被调用方service

1
2
3
4
5
// 查询特定天的注册人数
@Override
public Integer countRegister(String day) {
return baseMapper.countRegisterByDay(day);
}

被调用方mapper

1
2
3
4
5
6
7
8
9
10
<?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.atguigu.educenter.mapper.UcenterMemberMapper">
<!-- 查询特定天的注册人数 -->
<select id="countRegisterByDay" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM ucenter_member uc
WHERE DATE(uc.gmt_create) = #{day}
</select>
</mapper>

远程调用接口

1
2
3
4
5
6
7
@Component
@FeignClient("service-ucenter")
public interface UcenterClient {
// 查询特定天的注册人数
@GetMapping("/educenter/member/countRegister/{day}")
public Integer countRegister(@PathVariable("day") String day);
}

调用方controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/staservice/sta")
@CrossOrigin
public class StatisticsDailyController {

@Autowired
private StatisticsDailyService staService;

// 统计特定天的注册人数
@PostMapping("countRegister/{day}")
public R countRegister(@PathVariable String day) {
staService.countRegister(day);
return R.ok();
}
}

调用方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
@Service
public class StatisticsDailyServiceImpl extends ServiceImpl<StatisticsDailyMapper, StatisticsDaily> implements StatisticsDailyService {

@Autowired
private UcenterClient ucenterClient;

@Override
public void countRegister(String day) {
// 先删除相同日期的记录
QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();
wrapper.eq("date_calculated", day);
baseMapper.delete(wrapper);
// 获得指定日期的统计数
Integer count = ucenterClient.countRegister(day);
// 把统计数据添加到统计表当中
StatisticsDaily statisticsDaily = new StatisticsDaily();
statisticsDaily.setRegisterNum(count);
statisticsDaily.setDateCalculated(day);
// 一下是一些随机数
statisticsDaily.setVideoViewNum(RandomUtils.nextInt(100, 200));
statisticsDaily.setLoginNum(RandomUtils.nextInt(100, 200));
statisticsDaily.setCourseNum(RandomUtils.nextInt(100, 200));
// 插入数据库
baseMapper.insert(statisticsDaily);
}
}

生成统计数据(前端)

router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 统计分析模块
{
path: '/sta',
component: Layout,
redirect: '/sta/create',
name: '统计分析',
meta: { title: '统计分析', icon: 'example' },
children: [
{
path: 'create',
name: '生成数据',
component: () => import('@/views/statistics/create'),
meta: { title: '生成数据', icon: 'table' }
},
{
path: 'show',
name: '图表显示',
component: () => import('@/views/statistics/show'),
meta: { title: '图表显示', icon: 'tree' }
}
]
},

api

1
2
3
4
5
6
7
8
9
10
11
import request from '@/utils/request'

export default {
// 统计指定日期的注册数
createCountRegister(day) {
return request({
url: `/staservice/sta/countRegister/${day}`,
method: 'post'
})
}
}

vue模板与方法

1
<template>  <div class="app-container">    <!--表单-->    <el-form :inline="true" class="demo-form-inline">      <el-form-item label="日期">        <!-- 这里绑定了day -->        <el-date-picker          v-model="day"          type="date"          placeholder="选择要统计的日期"          value-format="yyyy-MM-dd"        />      </el-form-item>      <el-button :disabled="btnDisabled" type="primary" @click="create()"        >生成</el-button      >    </el-form>  </div></template><script>  import staApi from '@/api/statistics'  export default {    data() {      return {        day: '',        btnDisabled: false      }    },    created() {},    methods: {      create() {        staApi.createCountRegister(this.day).then(result => {          // 提示信息          this.$message({            type: 'success',            message: '生成数据成功!'          })          // 跳转到图表显示页面          this.$router.push({ path: '/sta/show' })        })      }    }  }</script>

定时任务

在固定时候自动执行程序

配置

在启动类上添加注解@EnableScheduling

1
@EnableScheduling   // 使用自动任务public class StatisticsApplication {    public static void main(String[] args) {        SpringApplication.run(StatisticsApplication.class, args);    }}

定时任务类

使用cron表达式,设置执行规则

1
@Componentpublic class ScheduledTask {    @Autowired    private StatisticsDailyService staService;    // 0/5 * * * * ?表示每隔5秒执行一次这个方法    @Scheduled(cron = "0/5 * * * * ?")    public void task1() {        System.out.println("**************task1执行了..");    }    //在每天凌晨1点,把前一天数据进行数据查询添加    @Scheduled(cron = "0 0 1 * * ?")    public void task2() {        staService.countRegister(DateUtil.formatDate(DateUtil.addDays(new Date(), -1)));    }}

日期工具类

1
public class DateUtil {    private static final String dateFormat = "yyyy-MM-dd";    /**     * 格式化日期     *     * @param date     * @return     */    public static String formatDate(Date date) {        SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);        return sdf.format(date);    }    /**     * 在日期date上增加amount天 。     *     * @param date   处理的日期,非null     * @param amount 要加的天数,可能为负数     */    public static Date addDays(Date date, int amount) {        Calendar now =Calendar.getInstance();        now.setTime(date);        now.set(Calendar.DATE,now.get(Calendar.DATE)+amount);        return now.getTime();    }    public static void main(String[] args) {        System.out.println(DateUtil.formatDate(new Date()));        System.out.println(DateUtil.formatDate(DateUtil.addDays(new Date(), -1)));    }}

统计分析图表显示(后端+前端)

使用echarts

后端

controller

1
// echatrts图表显示@GetMapping("showChartData/{type}/{begin}/{end}")public R getChartData(@PathVariable String begin, @PathVariable String end, @PathVariable String type) {    Map<String, Object> map = staService.getChartData(type, begin, end);    return R.ok().data(map);}

service

1
// echatrts图表显示@Overridepublic Map<String, Object> getChartData(String type, String begin, String end) {    // 指定日期范围    QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();    wrapper.between("date_calculated", begin, end);    // 这里要求前端的传递的value和表中对应字段值相同,select指定查询列    wrapper.select("date_calculated", type);    List<StatisticsDaily> staDataList = baseMapper.selectList(wrapper);    //前端要求数组json结构,对应后端java代码是list集合    //创建两个list集合,一个日期list,一个数量list    List<String> dateList = new ArrayList<>();    List<Integer> numList = new ArrayList<>();    //遍历查询所有数据list集合,进行封装    for (int i = 0; i < staDataList.size(); i++) {        StatisticsDaily daily = staDataList.get(i);        //封装日期list集合        dateList.add(daily.getDateCalculated());        //封装对应选择的数量        switch (type) {            case "login_num":                numList.add(daily.getLoginNum());                break;            case "register_num":                numList.add(daily.getRegisterNum());                break;            case "video_view_num":                numList.add(daily.getVideoViewNum());                break;            case "course_num":                numList.add(daily.getCourseNum());                break;            default:                break;        }    }    //把封装之后两个list集合放到map集合,进行返回    Map<String, Object> map = new HashMap<>();    map.put("dateList", dateList);    map.put("numList", numList);    return map;}

前端

引入npm依赖

npm install –save echarts@4.1.0

api

这里没有使用RequestBody传递对象,而是直接前端调用的时候传递对象取对应值

1
// 获得图表数据    getChartData(searchObj) {        return request({            url: `/staservice/sta/getChartData/${searchObj.type}/${searchObj.begin}/${searchObj.end}`,            method: 'get'        })    }

vue模板和方法

1
<template>  <div class="app-container">    <!--表单-->    <el-form :inline="true" class="demo-form-inline">      <el-form-item>        <el-select v-model="searchObj.type" clearable placeholder="请选择">          <el-option label="学员登录数统计" value="login_num" />          <el-option label="学员注册数统计" value="register_num" />          <el-option label="课程播放数统计" value="video_view_num" />          <el-option label="每日课程数统计" value="course_num" />        </el-select>      </el-form-item>      <el-form-item>        <el-date-picker          v-model="searchObj.begin"          type="date"          placeholder="选择开始日期"          value-format="yyyy-MM-dd"        />      </el-form-item>      <el-form-item>        <el-date-picker          v-model="searchObj.end"          type="date"          placeholder="选择截止日期"          value-format="yyyy-MM-dd"        />      </el-form-item>      <el-button        :disabled="btnDisabled"        type="primary"        icon="el-icon-search"        @click="showChart()"        >查询</el-button      >    </el-form>    <div class="chart-container">      <div id="chart" class="chart" style="height: 500px; width: 100%" />    </div>  </div></template><script>  import echarts from 'echarts'  import staApi from '@/api/statistics'  export default {    data() {      return {        searchObj: {},        btnDisabled: false,        xData: [],        yData: []      }    },    methods: {      showChart() {        staApi.getChartData(this.searchObj).then(result => {          this.yData = result.data.numList          this.xData = result.data.dateList          // 调用下面生成图表的方法,改变值          this.setChart()        })      },      // 默认显示图表方法      setChart() {        // 基于准备好的dom,初始化echarts实例        this.chart = echarts.init(document.getElementById('chart'))        // console.log(this.chart)        // 指定图表的配置项和数据        var option = {          title: {            text: '数据统计'          },          tooltip: {            trigger: 'axis'          },          dataZoom: [            {              show: true,              height: 30,              xAxisIndex: [0],              bottom: 30,              start: 10,              end: 80,              handleIcon:                'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',              handleSize: '110%',              handleStyle: {                color: '#d3dee5'              },              textStyle: {                color: '#fff'              },              borderColor: '#90979c'            },            {              type: 'inside',              show: true,              height: 15,              start: 1,              end: 35            }          ],          // x轴是类目轴(离散数据),必须通过data设置类目数据          xAxis: {            type: 'category',            data: this.xData          },          // y轴是数据轴(连续数据)          yAxis: {            type: 'value'          },          // 系列列表。每个系列通过 type 决定自己的图表类型          series: [            {              // 系列中的数据内容数组              data: this.yData,              // 折线图              type: 'line'            }          ]        }        this.chart.setOption(option)      }    }  }</script>

Canal数据同步

将远程数据库中的数据同步到本地数据库中,一般实际中项目有很多个数据库(比如说统计分析专门一个数据库),其与远程服务调用调用的区别是,该同步耦合度低,效率更高。

linux配置与开启

  1. linux中需要安装mysql(本地Windows也需要mysql)
  2. 创建数据库表(结构需要一样)

linux中docker中使用mysql一些常见命令

  1. su root
  2. docker ps
  3. docker start mysql
  4. docker exec -it mysql bash
  5. mysql -u root -p
  6. 可以使用命令行,也可以直接用配好的Navicat(实战5中配置redis的时候已经配置好了mysql)
  7. exit
  8. docker stop CONTAINER ID

创建数据库表

数据库guli,表test_canal

注意结构需要一样

mysql开启binlog

  1. 开启并进入mysql
  2. show variables like ‘log_bin’; 查询是否开启binlog
  3. 修改my.cnf(如果使用docker,那么要去挂载的目录下修改),具体如下篇文章
  4. https://www.freebytes.net/it/java/dokcer-mysql-binlog.htmlz

这里花了大概半个小时吧,还是个人博客文章靠谱,那些CSDN的文章都是炒来炒去的,这篇文章很清楚地讲了你挂载的目录是前半部分(我看CSDN那些文章总以为是配置挂载的时候的后半部分,所以弄了好久都没有用,看了这篇文章几分钟就解决了)

安装并配置canal

linux中下载然后解压

具体配置见视频

开启canal即可,进入bin文件夹下sh startup.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
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
</dependency>

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

<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
</dependency>
</dependencies>

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
# 服务端口
server.port=10000
# 服务名
spring.application.name=canal-client

# 环境设置:dev、test、prod
spring.profiles.active=dev

# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456789

canal客户端类

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
@Component
public class CanalClient {

//sql队列
private Queue<String> SQL_QUEUE = new ConcurrentLinkedQueue<>();

@Resource
private DataSource dataSource;

/**
* canal入库方法
*/
public void run() {

CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.128.129",
11111), "example", "", "");
int batchSize = 1000;
try {
connector.connect();
connector.subscribe(".*\\..*");
connector.rollback();
try {
while (true) {
//尝试从master那边拉去数据batchSize条记录,有多少取多少
Message message = connector.getWithoutAck(batchSize);
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
Thread.sleep(1000);
} else {
dataHandle(message.getEntries());
}
connector.ack(batchId);

//当队列里面堆积的sql大于一定数值的时候就模拟执行
if (SQL_QUEUE.size() >= 1) {
executeQueueSql();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
} finally {
connector.disconnect();
}
}

/**
* 模拟执行队列里面的sql语句
*/
public void executeQueueSql() {
int size = SQL_QUEUE.size();
for (int i = 0; i < size; i++) {
String sql = SQL_QUEUE.poll();
System.out.println("[sql]----> " + sql);

this.execute(sql.toString());
}
}

/**
* 数据处理
*
* @param entrys
*/
private void dataHandle(List<Entry> entrys) throws InvalidProtocolBufferException {
for (Entry entry : entrys) {
if (EntryType.ROWDATA == entry.getEntryType()) {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
EventType eventType = rowChange.getEventType();
if (eventType == EventType.DELETE) {
saveDeleteSql(entry);
} else if (eventType == EventType.UPDATE) {
saveUpdateSql(entry);
} else if (eventType == EventType.INSERT) {
saveInsertSql(entry);
}
}
}
}

/**
* 保存更新语句
*
* @param entry
*/
private void saveUpdateSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> newColumnList = rowData.getAfterColumnsList();
StringBuffer sql = new StringBuffer("update " + entry.getHeader().getTableName() + " set ");
for (int i = 0; i < newColumnList.size(); i++) {
sql.append(" " + newColumnList.get(i).getName()
+ " = '" + newColumnList.get(i).getValue() + "'");
if (i != newColumnList.size() - 1) {
sql.append(",");
}
}
sql.append(" where ");
List<Column> oldColumnList = rowData.getBeforeColumnsList();
for (Column column : oldColumnList) {
if (column.getIsKey()) {
//暂时只支持单一主键
sql.append(column.getName() + "=" + column.getValue());
break;
}
}
SQL_QUEUE.add(sql.toString());
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}

/**
* 保存删除语句
*
* @param entry
*/
private void saveDeleteSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> columnList = rowData.getBeforeColumnsList();
StringBuffer sql = new StringBuffer("delete from " + entry.getHeader().getTableName() + " where ");
for (Column column : columnList) {
if (column.getIsKey()) {
//暂时只支持单一主键
sql.append(column.getName() + "=" + column.getValue());
break;
}
}
SQL_QUEUE.add(sql.toString());
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}

/**
* 保存插入语句
*
* @param entry
*/
private void saveInsertSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> columnList = rowData.getAfterColumnsList();
StringBuffer sql = new StringBuffer("insert into " + entry.getHeader().getTableName() + " (");
for (int i = 0; i < columnList.size(); i++) {
sql.append(columnList.get(i).getName());
if (i != columnList.size() - 1) {
sql.append(",");
}
}
sql.append(") VALUES (");
for (int i = 0; i < columnList.size(); i++) {
sql.append("'" + columnList.get(i).getValue() + "'");
if (i != columnList.size() - 1) {
sql.append(",");
}
}
sql.append(")");
SQL_QUEUE.add(sql.toString());
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}

/**
* 入库
*
* @param sql
*/
public void execute(String sql) {
Connection con = null;
try {
if (null == sql) return;
con = dataSource.getConnection();
QueryRunner qr = new QueryRunner();
int row = qr.execute(con, sql);
System.out.println("update: " + row);
} catch (SQLException e) {
e.printStackTrace();
} finally {
DbUtils.closeQuietly(con);
}
}
}

启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootApplication
public class CanalApplication implements CommandLineRunner {
@Resource
private CanalClient canalClient;

public static void main(String[] args) {
SpringApplication.run(CanalApplication.class, args);
}

@Override
public void run(String... strings) throws Exception {
//项目启动,执行canal客户端监听
canalClient.run();
}
}

测试

TODO,这里application启动失败,去canal的log里看,说连接mysql的ip失败了,原因是address already in use,

然后我改了重启还是这个原因,本来没有被占用的端口启动了之后就被占用了,但是canal还是启动失败,就离谱,

总之原因是canal没有启动,address already in use

还是没解决,放弃了(我不理解!!!)

SpringCloud微服务

Gateway网关

概念

微服务架构之下不同的微服务有不同的网络地址,外部客户端需要调用多个服务的接口才能完成一个业务需求,但是如果直接让客户端与各个服务器通信,会出现许多问题,所以这些问题需要借助API网关解决。

网关是结余客户端和服务端之间的之间层,所有外部请求都会先经过API网关这一层,所以网关实现安全,性能,监控等操作。

就是之前的nginx的增强版

image-20211006164431906

image-20211006171309513

创建模块

创建模块infrastructure,子模块api_gateway

依赖

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
<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>common_utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

配置文件

配置服务按如下配置即可

也可以使用YAML格式,更方便不用写下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 服务端口
server.port=8222
# 服务名
spring.application.name=service-gateway
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true
#设置路由id
spring.cloud.gateway.routes[0].id=service-acl
#设置路由的uri lb://nacos注册服务名称
spring.cloud.gateway.routes[0].uri=lb://service-acl
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[0].predicates=Path=/*/acl/**
#配置service-edu服务
spring.cloud.gateway.routes[1].id=service-edu
spring.cloud.gateway.routes[1].uri=lb://service-edu
spring.cloud.gateway.routes[1].predicates=Path=/eduservice/**
#配置service-edu服务
spring.cloud.gateway.routes[2].id=service-msm
spring.cloud.gateway.routes[2].uri=lb://service-msm
spring.cloud.gateway.routes[2].predicates=Path=/edumsm/**

启动类

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient //nacos
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

测试:使用网关的端口号即可访问已配置的微服务OK

跨域问题

在网关中统一开启跨域,cv一下固定config、filter、handler

注意controller中的跨域注解不能有,要不然两次跨域抵消

权限管理(后端)

![07 权限管理需求](https://reria-images.oss-cn-hangzhou.aliyuncs.com/img01/images-master/img/07 权限管理需求.png)

环境搭建

模块

service_acl,spring_security

依赖

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>spring_security</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>

整合接口

直接CV好家伙,好像是因为太复杂了吧,没事下个项目自己做

获取所有菜单

递归,构建树形结构(注意菜单可能有多级结构(之前做过两级的))

entity

注意这个实体类的属性,使用了递归存储children

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Permission implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "编号")
@TableId(value = "id", type = IdType.ID_WORKER_STR)
private String id;

@ApiModelProperty(value = "所属上级")
private String pid;

@ApiModelProperty(value = "层级")
@TableField(exist = false)
private Integer level;

@ApiModelProperty(value = "下级")
@TableField(exist = false)
private List<Permission> children;

}

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/admin/acl/permission")
public class PermissionController {
@Autowired
private PermissionService permissionService;
//获取全部菜单
@ApiOperation(value = "查询所有菜单")
@GetMapping
public R indexAllPermission() {
List<Permission> list = permissionService.queryAllMenuGuli();
return R.ok().data("children",list);
}
}

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//获取全部菜单
@Override
public List<Permission> queryAllMenuGuli() {
//1 查询菜单表所有数据
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
List<Permission> permissionList = baseMapper.selectList(wrapper);
//2 把查询所有菜单list集合按照要求进行封装
List<Permission> resultList = bulidPermission(permissionList);
return resultList;
}
//把返回所有菜单list集合进行封装的方法
public static List<Permission> bulidPermission(List<Permission> permissionList) {

//创建list集合,用于数据最终封装
List<Permission> finalNode = new ArrayList<>();
//把所有菜单list集合遍历,得到顶层菜单 pid=0菜单,设置level是1
for(Permission permissionNode : permissionList) {
//得到顶层菜单 pid=0菜单
if("0".equals(permissionNode.getPid())) {
//设置顶层菜单的level是1
permissionNode.setLevel(1);
//根据顶层菜单,向里面进行查询子菜单,封装到finalNode里面
finalNode.add(selectChildren(permissionNode,permissionList));
}
}
return finalNode;
}
private static Permission selectChildren(Permission permissionNode, List<Permission> permissionList) {
//1 因为向一层菜单里面放二层菜单,二层里面还要放三层,把对象初始化
permissionNode.setChildren(new ArrayList<Permission>());

//2 遍历所有菜单list集合,进行判断比较,比较id和pid值是否相同
for(Permission it : permissionList) {
//判断 id和pid值是否相同
if(permissionNode.getId().equals(it.getPid())) {
//当前level=父菜单的level值+1
int level = permissionNode.getLevel()+1;
it.setLevel(level);
//如果children为空,进行初始化操作
//if(permissionNode.getChildren() == null) {
// permissionNode.setChildren(new ArrayList<Permission>());
//}
//把查询出来的子菜单放到父菜单里面
permissionNode.getChildren().add(selectChildren(it,permissionList));
}
}
return permissionNode;
}

测试

SwaggerConfig类中有一行代码需要注释才能测试,,表示路径中有这个关键字则不让测试,可以注释掉这行代码也可以修改controller的路径

1
2
3
4
5
6
7
8
9
10
11
@Bean
public Docket webApiConfig() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
// .paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();

}

删除菜单

递归删除,删除上级菜单下级菜单也要同时删除

controller

1
2
3
4
5
6
@ApiOperation(value = "递归删除菜单")
@DeleteMapping("remove/{id}")
public R remove(@PathVariable String id) {
permissionService.removeChildByIdGuli(id);
return R.ok();
}

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
//============递归删除菜单==================================
@Override
public void removeChildByIdGuli(String id) {
//1 创建list集合,用于封装所有删除菜单id值
List<String> idList = new ArrayList<>();
//2 向idList集合设置删除菜单id
this.selectPermissionChildById(id, idList);
//把当前id封装到list里面
idList.add(id);
baseMapper.deleteBatchIds(idList);
}

//2 根据当前菜单id,查询菜单里面子菜单id,封装到list集合
private void selectPermissionChildById(String id, List<String> idList) {
//查询菜单里面子菜单id
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.eq("pid", id);
wrapper.select("id");
List<Permission> childIdList = baseMapper.selectList(wrapper);
//把childIdList里面菜单id值获取出来,封装idList里面,做递归查询
childIdList.stream().forEach(item -> {
//封装idList里面
idList.add(item.getId());
//递归查询
this.selectPermissionChildById(item.getId(), idList);
});
}

角色分配权限

controller

1
2
3
4
5
6
@ApiOperation(value = "给角色分配权限")
@PostMapping("/doAssign")
public R doAssign(String roleId,String[] permissionId) {
permissionService.saveRolePermissionRealtionShipGuli(roleId,permissionId);
return R.ok();
}

service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//=========================给角色分配菜单=======================
@Override
public void saveRolePermissionRealtionShipGuli(String roleId, String[] permissionIds) {
//roleId角色id
//permissionId菜单id 数组形式
//1 创建list集合,用于封装添加数据
List<RolePermission> rolePermissionList = new ArrayList<>();
//遍历所有菜单数组
for (String perId : permissionIds) {
//RolePermission对象
RolePermission rolePermission = new RolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(perId);
//封装到list集合
rolePermissionList.add(rolePermission);
}
//添加到角色菜单关系表
rolePermissionService.saveBatch(rolePermissionList);
}

SpringSecurity

介绍

spring-security实现功能如下:

  1. 用户认证:用户登录时查询数据库验证用户名和密码
  2. 用户授权:登录之后给予用户对应的权限和操作功能

spring-security本质上是filter,对请求进行过滤:

  • 基于session,则对cookie中的sessionid进行解析找到服务器存储的session信息然后判断当前用户是否符合请求的要求
  • 基于token,则解析出token然后将当前请求加入到spring-security管理的权限信息当中

认证和授权实现思路

如果系统的模块众多,每个模块都需要就行授权与认证,所以我们选择基于token的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息生成token返回,浏览器将token记录到cookie中,每次调用api接口都默认将token携带到header请求头中,Spring-security解析header头获取token信息,解析token获取当前用户名,根据用户名就可以从redis中获取权限列表,这样Spring-security就能够判断当前请求是否有权限访问

image-20211007145558209

整合

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>common_utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>

项目结构

6aafe337-8a5d-440c-b93d-ff60d4f9cef6

测试

妈耶太吓人了幸好我备份了(为啥git不先讲),试了下整合,菜单栏还是显示不出来,先恢复了,以后再试

Nacos配置管理

Git版本管理

创建本地git仓库:idea -> VCS -> Create Git Repository

将文件加到本地仓库:idea -> 父项目名 -> 右键 -> git -> add

添加远程仓库地址:idea -> 父项目名 -> 右键 -> git -> git remotes

将文件提交至远程仓库:commit

上传:push

评论




🧡💛💚💙💜🖤🤍