Day 16(07.13)
统计分析模块(后台)
准备工作
- 创建统计表
-
创建service_statistics模块
-
使用代码生成器生成代码
-
创建启动类
@SpringBootApplication @ComponentScan(basePackages = {"com.lujin"}) @EnableDiscoveryClient @EnableFeignClients @MapperScan("com.lujin.staservice.mapper") public class StaApplication { public static void main(String[] args) { SpringApplication.run(StaApplication.class, args); } }
在service_ucenter模块创建接口用来查询某一天注册人数,然后在service_statistics模块远程调用这个接口得到注册人数,统计到统计表中
生成统计数据
-
在service_ucenter模块创建接口,统计某一天的注册人数
//查询某一天注册人数 @GetMapping("countRegister/{day}") public R countRegister(@PathVariable String day){ Integer count=memberService.countRegisterDay(day); return R.ok().data("countRegister",count); }
-
service
//查询某一天注册人数 @Override public Integer countRegisterDay(String day) { return baseMapper.countRegisterDay(day); }
-
mapper
<!--查询某一天注册人数--> <select id="countRegisterDay" resultType="java.lang.Integer"> SELECT COUNT(*) FROM ucenter_member uc WHERE DATE (uc.gmt_create)=#{day} </select>
当接口中有多个参数时,取值可以根据位置取,如#{0},#{1}
也可以在接口中加入注解@Param,然后根据注解后的名字取,如:
Integer countRegisterDay(@Param("aa") String day,@Param("bb") String d);
通过#{aa}取day的值
-
在service_statistics创建远程调用接口
@Component @FeignClient("service-ucenter") public interface UcenterClient { //查询某一天注册人数 @GetMapping("/educenter/member/countRegister/{day}") public R countRegister(@PathVariable("day") String day); }
-
在service_statistics远程调用
-
controller
@RestController @RequestMapping("/staservice/sta") public class StatisticsDailyController { @Autowired private StatisticsDailyService staService; //统计某一天注册人数,生成统计数据 @PostMapping("registerCount/{day}") public R registerCOunt(@PathVariable String day){ staService.registerCount(day); return R.ok(); } }
-
service
@Service public class StatisticsDailyServiceImpl extends ServiceImpl<StatisticsDailyMapper, StatisticsDaily> implements StatisticsDailyService { @Autowired private UcenterClient ucenterClient; //统计某一天注册人数,生成统计数据 @Override public void registerCount(String day) { //添加数据之前,先删除表内相同日期的数据 QueryWrapper<StatisticsDaily> wrapper=new QueryWrapper<>(); wrapper.eq("data_calculated",day); baseMapper.delete(wrapper); //远程调用统计某一天注册人数 R registerR=ucenterClient.countRegister(day); Integer countRegister = (Integer)registerR.getData().get("countRegister"); //把统计数据添加到数据库,统计分析表里面 StatisticsDaily sta=new StatisticsDaily(); sta.setRegisterNum(countRegister);//注册人数 sta.setDateCalculated(day);//统计日期 sta.setVideoViewNum(RandomUtils.nextInt(100,200)); sta.setLoginNum(RandomUtils.nextInt(100,200)); sta.setCourseNum(RandomUtils.nextInt(100,200)); baseMapper.insert(sta); } }
-
图表显示数据
定时任务
在固定时间自动执行程序
步骤
-
在启动类添加注解@EnableScheduling
-
创建定时任务类,在类中使用表达式设置何时执行
cron表达式,设置执行规则
例子:
@Component public class ScheduledTask { //每天凌晨一点,把前一天数据查询结果进行添加 @Scheduled(cron = "0 0 1 * * ?") public void task2() { //获取上一天的日期 staService.registerCount(DateUtil.formatDate(DateUtil.addDays(new Date(),-1))); } }
cron表达式设置规则
七子表达式或七域表达式
前端
-
创建路由
后台的前端代码中src/router/index.js
{ path: '/sta', component: Layout, redirect: '/sta/create', name: '统计分析', meta: { title: '统计分析', icon: 'example' }, children: [ { path: 'create', name: '生成数据', component: () => import('@/views/sta/create'), meta: { title: '生成数据', icon: 'table' } }, { path: 'show', name: '图表显示', component: () => import('@/views/sta/show'), meta: { title: '图表显示', icon: 'tree' } } ] },
-
创建页面src/views/sta//create.vue和src/views/sta//show.vue
-
create.vue
<script> import sta from '@/api/sta' export default { data() { return { day:'', btnDisabled: false } }, created() { }, methods:{ create() { sta.createStaData(this.day) .then(response => { //提示信息 this.$message({ type: 'success', message: '生成数据成功!' }) //跳转到图表显示页面 this.$router.push({path:'/sta/show'}) }) } } }
-
-
show.vue
showChart() { staApi.getDataSta(this.searchObj) .then(response => { console.log('*****************'+response) this.yData = response.data.numDataList this.xData = response.data.date_calculatedList //调用下面生成图表的方法,改变值 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) }
-
创建src/api/sta.js调用接口方法
import request from '@/utils/request' export default { //1 生成统计数据 createStaData(day) { return request({ url: '/staservice/sta/registerCount/'+day, method: 'post' }) }, //2 获取统计数据 getDataSta(searchObj) { return request({ url: `/staservice/sta/showData/${searchObj.type}/${searchObj.begin}/${searchObj.end}`, method: 'get' }) } }
项目整合ECharts
-
controller
//图表显示,返回两部分数据,日期json数组,数量json数组 @GetMapping("showData/{type}/{begin}/{end}") public R showData(@PathVariable String type,@PathVariable String begin, @PathVariable String end){ Map<String,Object> map= staService.getShowData(type,begin,end); return R.ok().data(map); }
-
service
//图表显示,返回两部分数据,日期json数组,数量json数组 @Override public Map<String, Object> getShowData(String type, String begin, String end) { //根据条件查询对应的数据 QueryWrapper<StatisticsDaily> wrapper=new QueryWrapper<>(); wrapper.between("data_calculated",begin,end); wrapper.select("data_calculated",type); List<StatisticsDaily> staList=baseMapper.selectList(wrapper); //因为返回的有两部分数据,日期 和日期对应的数量 //后端的list集合会变成前端的json数组形式[1,2,3],后端的map集合或者对象会变成json对象形式["name":"ll","age":20] //前端要求json数组结构,对应后端list集合 //创建两个list集合,一个日期list,一个数量list List<String> data_calculatedList=new ArrayList<>(); List<Integer> numDataList=new ArrayList<>(); for (int i = 0; i < staList.size(); i++) { StatisticsDaily daily=staList.get(i); //封装日期list集合 data_calculatedList.add(daily.getDateCalculated()); //封装对应数量list集合 switch (type) { case "register_num": numDataList.add(daily.getRegisterNum()); break; case "login_num": numDataList.add(daily.getLoginNum()); break; case "video_view_num": numDataList.add(daily.getVideoViewNum()); break; case "course_num": numDataList.add(daily.getCourseNum()); break; default: break; } } //把封装之后的两个list集合放到map集合,进行返回 Map<String, Object> map=new HashMap<>(); map.put("data_calculatedList",data_calculatedList); map.put("numDataList",numDataList); return map; }
Day 17(07.14)
Canal
数据同步工具:把远程库的内容同步到本地进行操作
同步过程的准备工作
-
两个系统,在Linux系统安装MySQL数据库,本地Windows系统也安装MySQL
-
两个数据库和数据表的名称和表结构一样
-
修改Linux配置
-
检查binlog是否开启
show variables like 'log_bin';
-
修改MySQL配置文件my.cnf
vi /etc/my.cnf #添加一下语句 log-bin=mysql-bin #binlog文件名 binlog_format=ROW #选择row模式 server_id=1 #mysql实例id,不能和canal的slaveId重复
-
重启 mysql
service mysql restart
-
-
在Linux系统安装canal工具
解压后修改配置文件conf/example/instance.properties
#需要改成自己的数据库信息 canal.instance.master.address=192.168.44.132:3306 #需要改成自己的数据库用户名与密码 canal.instance.dbUsername=canal canal.instance.dbPassword=canal #需要改成同步的数据库表规则,例如只是同步一下表 #canal.instance.filter.regex=.*\\..* canal.instance.filter.regex=guli_ucenter.ucenter_member
代码编写
-
创建模块canal_clientedu
-
引入依赖
<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>
-
创建application.properties配置文件
# 服务端口 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=root
-
创建canal客户端类,在启动类执行
-
修改启动类
@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(); } }
SpringCloud
服务网关:GateWay
GateWay网关
客户端到服务端中的中间件,起到请求转发,负载均衡,权限控制等作用
需要将网关和各个服务都在nacos注册中心进行注册,才能实现调用
(1)路由。路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配
(2)断言。Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。
(3)过滤器。一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理
网关使用
- 创建微服务模块
-
引入依赖
<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>
-
创建启动类,创建配置文件
-
启动类
@SpringBootApplication @EnableDiscoveryClient public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class,args); } }
-
配置文件
#使用服务发现路由 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/**
-
权限管理模块
权限管理需求
表和表之间的关系
菜单管理
-
菜单列表
查询所有菜单,把菜单所有数据查询出来,构建树形结构。和之前分类列表相似,使用递归实现查询所有菜单功能
controller
//获取全部菜单 @ApiOperation(value = "查询所有菜单") @GetMapping public R indexAllPermission() { List<Permission> list = permissionService.queryAllMenuGuli(); return R.ok().data("children",list); }
service
//获取全部菜单 @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值+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; }
-
菜单添加、修改
-
菜单删除
controller
@ApiOperation(value = "递归删除菜单") @DeleteMapping("remove/{id}") public R remove(@PathVariable String id) { permissionService.removeChildByIdGuli(id); return R.ok(); }
service
@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
@ApiOperation(value = "给角色分配权限") @PostMapping("/doAssign") public R doAssign(String roleId,String[] permissionId) { permissionService.saveRolePermissionRealtionShipGuli(roleId,permissionId); return R.ok(); }
service
@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); }
用户管理
- 添加、删除、修改、查询
- 为用户分配角色
开发权限管理典型接口
-
创建子模块service_acl
-
引入依赖
<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>
Day 18(07.15)
整合Spring Security权限框架
Spring Security
主要包含两部分内容:用户认证和用户授权
本质是一个过滤器,对请求进行过滤
用户认证:用户登录时,查询数据库,输入的用户名和密码是否正确,正确则认证成功
用户授权:登录用户可能是不同角色,不同角色可以操作的功能不同。
(1)如果是基于Session,那么Spring-security会对cookie里的sessionid进行解析,找到服务器存储的sesion信息,然后判断当前用户是否符合请求的要求。
(2)如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限信息中去
认证授权流程
如果系统的模块众多,每个模块都需要就行授权与认证,所以我们选择基于token的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息生成token返回,浏览器将token记录到cookie中,每次调用api接口都默认将token携带到header请求头中,Spring-security解析header头获取token信息,解析token获取当前用户名,根据用户名就可以从redis中获取权限列表,这样Spring-security就能够判断当前请求是否有权限访问
代码部分
-
在spring_security引入相关依赖
<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>
-
代码结构
Nacos配置中心
Nacos 可以与 Spring, Spring Boot, Spring Cloud 集成,并能代替 Spring Cloud Eureka, Spring Cloud Config。通过 Nacos Server 和 spring-cloud-starter-alibaba-nacos-config 实现配置的动态变更。
nacos配置中心:系统配置的集中管理(编辑、存储、分发)、动态更新不重启、回滚配置(变更管理、历史版本管理、变更审计)等所有与配置相关的活动。
在系统开发过程中,开发者通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成。配置变更是调整系统运行时的行为的有效手段
Data ID 的完整规则格式如下
p r e f i x − {prefix}- prefix−{spring.profiles.active}.${file-extension}
- prefix:服务名称。
- spring.profiles.active=dev 即为当前环境对应的 profile。 注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 p r e f i x . {prefix}. prefix.{file-extension}
- file-exetension 配置文件 类型
springboot配置文件加载顺序
-
bootstrap.properties
-
application.properties
spring.profiles.active=dev
-
application-dev.properties
项目配置
-
在调用的服务的resources下创建bootstrap.properties
#配置中心地址 spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.profiles.active=dev # 该配置影响统一配置中心中的dataId spring.application.name=service-statistics
-
调用的服务引入config依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
-
把之前的application.properties内容注释掉
名称空间切换环境
项目开发环境
dev:开发环境
test:测试环境
prod:生产环境
通过名称空间读取不同的配置文件
具体操作
-
新建命名空间
-
读取,修改配置文件
spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.profiles.active=dev # 该配置影响统一配置中心中的dataId,之前已经配置过 spring.application.name=service-statistics #修改这里的命名空间值 spring.cloud.nacos.config.namespace=13b5c197-de5b-47e7-9903-ec0538c9db01
多配置文件加载
修改配置文件bootstrap.properties,加入代码
spring.cloud.nacos.config.ext-config[0].data-id=redis.properties
# 开启动态刷新配置,否则配置文件修改,工程无法感知
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=jdbc.properties
spring.cloud.nacos.config.ext-config[1].refresh=true
提交项目到Git仓库
提交代码到码云
- 打开项目并点击菜单栏上的【VCS】–》【Import into version control】–》【Create Git Repository】创建本地仓库
- 在打开的【Create Git Repository】对话框内选择本地仓库的位置,这里选择项目的根目录
- 右击项目点击【Git】–》【Add】,接着点击【Git】–》【Commit Directory】在打开的窗口中选择要上传到本地仓库的代码并添加注释后提交到本地仓库内
- 右击项目点击【Git】–》【Repository】–》【Remotes…】。在打开的【Git Remotes】窗口中添加码云的远程仓库。码云的远程仓库地址可以在码云仓库内找到
- 上传代码到码云,右击项目点击【Git】–》【Repository】–》【Push…】在打开的【Push commits】内可以看到已提交到本地仓库的提交信息。点击【Push】按钮将本地仓库的代码上传到码云上,上传成功后就可以在码云上看到