智慧云智慧教育平台实战项目笔记

一、简介

课程内容:智慧云教育平台管理后台、智慧云教育平台学生端、项目的正式部署

1、技术说明

  • 后端技术:JDK1.8 + SpringBoot + MyBatis + Shiro
  • 缓存框架:Redis
  • 数据库:MySQL 5.7
  • 前端技术:Element-UI + Vue
  • 开发工具:IDEA 2019.3.3
  • 项目管理工具:Maven、Git

使用最主流的框架 SpringBoot + Vue 实现完全前后端分离

2、核心功能介绍

  • 管理后台核心功能:RBAC权限管理、试题管理、试卷批改
  • 学生端核心功能:考试中心、我的错题本

3、学习前提

  • 后端技术:掌握SpringBoot + MyBatis + Shiro + MySQL的基本使用
  • 前端技术:掌握Vue、Element-UI、CS6语法的基本使用
  • 热爱Java编程,喜欢研究新技术

4、课程收获

  • 加强对Java程序员基础知识的掌握
  • 掌握企业级项目编码规范,提升代码优化的能力
  • 掌握企业级 SpringBoot + Vue + Element-UI 全栈开发技能,增加项目经验,提升职场竞争力
  • 掌握项目从零搭建到项目正式部署的完整流程

二、Maven介绍及其配置

1、Maven是什么?

​ Maven是Apache下的一一个纯Java 开发的开源项目。它主要用来帮助实现项目的构建、测试、打包和部署。Maven提供了标准的软件生命周期模型和构建模型,通过配置就能对项目进行全面的管理。想了解更多点击这里

2、Maven的优势

  • Maven能够帮助我们快速构建和发布项目,提高工作效率
  • Maven能够非常方便的帮助我们管理jar包和解决jar包冲突
  • Maven对于目录结构有要求,约定优于配置,开发者在项目间切换就省去了学习成本
  • Maven有助于项目的多模块开发

3、Maven项目结构

目录 目的
${basedir} 存放pom.xml和所有的子目录
${basedir}/src/main/java 项目的java源代码
${basedir}/src/main/resources 项目的资源,比如说property文件,springmvc.xml
${basedir}/src/test/java 项目的测试类,比如说Junit代码
${basedir}/src/test/resources 测试用的资源
${basedir}/src/main/webapp/WEB-INF web应用文件目录,web项目的信息,比如存放web.xml、本地图片、jsp视图页面
${basedir}/target 打包输出目录
${basedir}/target/classes 编译输出目录
${basedir}/target/test-classes 测试编译输出目录
Test.java Maven只会自动运行符合该命名规则的测试类
~/.m2/repository Maven默认的本地仓库目录位置

4、Maven下载

Maven 下载地址:http://maven.apache.org/download.cgi

不同平台下载对应的包:

系统 包名
Windows apache-maven-3.3.9-bin.zip
Linux apache-maven-3.3.9-bin.tar.gz
Mac apache-maven-3.3.9-bin.tar.gz

下载包后解压到对应目录:

系统 存储位置 (可根据自己情况配置)
Windows D:\Maven\apache-maven-3.3.9
Linux /usr/local/apache-maven-3.3.9
Mac /usr/local/apache-maven-3.3.9

5、设置Maven环境变量

​ 右键 “计算机”,选择 “属性”,之后点击 “高级系统设置”,点击"环境变量",来设置环境变量,有以下系统变量需要配置:

新建系统变量 MAVEN_HOME,变量值:E:\Maven\apache-maven-3.3.9

编辑系统变量 Path,添加变量值:;%MAVEN_HOME%\bin

**注意:**注意多个值之间需要有分号隔开,然后点击确定。

6、修改Maven配置文件

打开E:\Maven\apache-maven-3.3.9\conf目录下的setting.xml文件

修改本地仓库路径:

<localRepository>D:\Apps\Maven-3.8.1\repository</localRepository>

修改镜像源为阿里云

    <mirror><id>nexus-aliyun</id><name>Nexus aliyun</name><url>http://maven.aliyun.com/nexus/content/groups/public</url><mirrorOf>central</mirrorOf></mirror>

修改编译器JDK版本为1.8

    <profile><id>jdk-1.8</id><activation><activeByDefault>true</activeByDefault><jdk>1.8</jdk></activation><properties><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion></properties></profile>

7、pom.xml文件中常见的标签使用

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><!-- 描述这个POM文件遵从哪个版本的项目描述符 --><modelVersion>4.0.0</modelVersion><!-- 项目组织的唯一标识 --><groupId>cn.jasondom.springboot</groupId><!-- 项目唯一标识 --><artifactId>Template</artifactId><!-- 项目版本号 --><version>1.0.0</version><!-- 项目打包类型jar、war、pom --><packaging>jar</packaging><!-- 项目名称 --><name>SpringBoot</name><!-- 项目介绍 --><description>SpringBoot is a lightweight Java Web Framework</description><!-- 项目地址 --><url>http://maven.apache.org</url><!-- 定义变量,通常用于定义依赖版本 --><properties><java.version>1.8</java.version><velocity.version>2.2.3</velocity.version></properties><!--jar包依赖列表--><dependencies><dependency><groupId>org.apache.velocity</groupId><artifactId>velocity</artifactId><version>${velocity.version}</version><!-- 当前依赖的作用范围:compile:当前依赖参与所有runtime:当前依赖仅参与项目的运行阶段provided:与compile类似,区别在于不参与项目的最终打包system:从本地磁盘中引用一个jar包(需要添加一个systemPath标签,用于指明jar包路径)test:当前依赖仅参与项目的单元测试--><scope></scope><!-- 排除jar包依赖列表 --><exclusions><!-- 排除的jar包1 --><exclusion><groupId>...</groupId><artifactId>...</artifactId></exclusion><!-- 排除的jar包2 --><exclusion><groupId>...</groupId><artifactId>...</artifactId></exclusion></exclusions></dependency></dependencies><!-- 指定继承父项目的标签 --><parent><groupId>...</groupId><artifactId>...</artifactId><version>...</version><!-- 指定父工程的pom.xml路径,此标签默认值为../pom.xml --><relativePath/></parent><!-- 指定子模块 --><modules><module>子项目1目录路径</module><module>子项目2目录路径</module><module>...</module></modules><!-- 编译配置 --><build><!--插件列表--><plugins><plugin><groupId></groupId><artifactId></artifactId></plugin></plugins></build></project>

8、多模块开发的好处

  • 降低项目复杂性,提升我们的开发效率
  • 有利于项目遵从高内聚,低耦合的设计模式,保证了代码的质量和健壮性
  • 避免重复造轮子,减少工作量

9、IDEA上配置Maven

File中打开Settings...,搜索Maven,根据安装目录修改配置

注意:建议使用Maven-3.5,如果版本过高可能与IDEA不兼容

为了避免所有新建项目都要修改Maven配置,可以打开File -> Other Settings -> Settings for New Project...重新设置一次Maven

三、后端项目环境的搭建

1、创建Maven类型的父工程



接下来点击Next -> Finish完成创建,等项目构建完成后删除src加粗样式目录

2、创建SpringBoot类型的管理模块

(1)、在工程目录上右键单击,选择New -> Module...

(2)、设置模块名称

(3)、选择基本依赖

(4)、点击Next -> Finish完成创建,将管理模块的SpringBoot父依赖剪切至父工程中,并将版本改为2.1.9.RELEASE

  <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.9.RELEASE</version><relativePath/></parent>

(5)、将管理模块的parent标签修改成以下内容,使其依赖于父工程

    <parent><groupId>com.education</groupId><artifactId>education</artifactId><version>1.0-SNAPSHOT</version><relativePath/></parent>

(6)、剪切管理模块的依赖列表至父工程

<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

(7)、将JDK版本改为1.8

  <properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target></properties>

3、创建Maven类型的service模块

(1)、在工程目录上右键单击,选择New -> Module...

(2)、设置模块名称(将GroupId设置为com.education.service

(3)、点击Next -> Finish完成创建,打开service模块下的pom.xml文件,修改groupId和父工程为如下内容

  <parent><groupId>com.education</groupId><artifactId>education</artifactId><version>1.0-SNAPSHOT</version><relativePath/></parent><groupId>com.education.service</groupId><artifactId>education-service</artifactId><version>1.0-SNAPSHOT</version>

4、创建common工具模块

(1)、在工程目录上右键单击,选择New -> Module...

(2)、设置模块名称(将GroupId设置为com.education.common

(3)、点击Next -> Finish完成创建,打开common模块下的pom.xml文件,修改groupId和父工程为如下内容

  <parent><groupId>com.education</groupId><artifactId>education</artifactId><version>1.0-SNAPSHOT</version><relativePath/></parent><groupId>com.education.common</groupId><artifactId>education-common</artifactId><version>1.0-SNAPSHOT</version>

5、修改依赖关系

(1)、添加模块到父工程,在父工程的pom.xml文件中添加如下内容

  <modules><module>education-admin-api</module><module>education-service</module><module>education-common</module></modules>

(2)、在admin-api模块中添加依赖

<dependency><groupId>com.education.common</groupId><artifactId>education-common</artifactId><version>1.0-SNAPSHOT</version>
</dependency><dependency><groupId>com.education.service</groupId><artifactId>education-service</artifactId><version>1.0-SNAPSHOT</version>
</dependency>

(3)在service模块中添加依赖

<dependency><groupId>com.education.common</groupId><artifactId>education-common</artifactId><version>1.0-SNAPSHOT</version>
</dependency>

6、测试环境

(1)、在管理模块src/main/resources/目录下创建如下三个文件(删除默认的application.properties文件)

配置文件名 说明
application.yml 默认环境配置文件
application-dev.yml 开发环境配置文件
application-prod.yml 生产环境配置文件

(2)、在application.yml中添加如下配置,激活开发环境配置文件

spring:profiles:active: dev

(3)、在application-dev.yml中添加如下配置,设置服务器访问端口号

server:port: 80

(4)、在com/education/admin/api/EducationAdminApiApplication.java入口类中编写测试方法test

@SpringBootApplication
@RestController
public class EducationAdminApiApplication {@GetMappingpublic String test() {return "success";}public static void main(String[] args) {SpringApplication.run(EducationAdminApiApplication.class, args);}
}

(5)、删除education-admin-api/src/test/java/com/education/admin/api包下的test类后,启动main函数

(6)、在浏览器中访问localhost,如果提示success表示环境搭建成功

7、Maven中常用的命令

指令 功能
clean 清除编译后的class文件
compile 编译项目的源代码
test 对项目进行单元测试
package 对项目进行打包
install 对项目进行打包,并安装到本地仓库

四、Git简介及基本使用

1、Git简介

1.1 Git是什么?

Git是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。

1.2 为什么要使用Git?

  • 基于分布式的设计,有利于项目的多人合作开发,提高工作效率
  • 方便开发者解决代码冲突
  • 可以从当前版本回退到任意版本,防止误操作导致代码丢失

1.3 Git的工作流程

1.4 Git分支的概念

可以理解成一条条的河流,最终都要流入大海(master)

1.5 Git分支结构

2、下载并配置Git

2.1 下载Git

Git 各平台安装包下载地址为:http://git-scm.com/downloads

2.2 在IDEA上配置Git

File中打开Settings...,搜索Git,根据安装目录修改配置

3、将代码上传至码云仓库

3.2 项目码云步骤

  • 注册码云账号 -> https://gitee.com/

  • 使用码云创建一个远程仓库

  • 在IDEA中将代码上传至码云仓库

3.1 IDEA中将代码上传至码云仓库

1)选择Create Git Repository...,在弹出的对话框中选择项目路径

2)将代码提交至本地仓库


选中要提交的文件,输入提交信息,点击Commit即可提交到本地仓库(只提交源码和配置文件,.idea.mvn和编译等文件不用提交)

3)将项目提交至远程仓库

点击Define remote,输入远程仓库的地址点击OK,选择Push即可提交代码至远程仓库

4、代码的更新

(1)、可以直接在码云线上修改代码

(2)添加一个测试方法,并提交

(3)按图示操作,即可更新代码到本地(有的IDEA有多个选项,可以选择MergeBranch Default,但推荐选择Merge,因为Merge不仅可以更新代码,在一般情况下,它还会自动帮忙合并代码)

(4)测试完成后将test测试方法删除,重新push到码云(图示中的Commit and Push可以同时将代码提交到本地和远程仓库,而Commit只能提交到本地)

5、使用Git解决代码冲突

当多个人修改了同一方法,就会产生代码冲突,下面通过线上线下同时修改代码模拟多人修改同一方法的场景

(1)、线上修改代码并提交

(2)、线下修改代码并提交

提交代码,选择Commit and Push,弹出对话框后继续选择Commit and Push,再选择Push

(3)、此时会弹出拒绝提交,需要合并代码的提示,选择Merge

(4)、弹出冲突提示,继续选择Merge

(5)、此时会弹出冲突代码的对比窗口,可以点击第15行代码处指向中间窗口的箭头(>><<)来控制合并代码,选择好后点击Apply,再按快捷键Ctrl + Shift + K 重新提交代码

6、将远程仓库代码导入到本地

(1)、从远程仓库新建项目

(2)、输入远程仓库地址,选择项目的存放路径,点击Clone,弹出提示后点击Yes

(3)、你会发现IDEA没有自动打开项目,可以通过File -> Open -> 选择项目的pom.xml文件打开,此时会多次弹出提示框,按提示操作即可,右下角弹出提示后选择Add as Maven Project,至此,项目就成功导入了

7、Git分支的使用

可通过右下角的Git xxx按钮查看当前分支

Local Branches表示本地分支;Remote Branches表示远程仓库分支;按钮中的xxx表示当前分支名称;可以通过New Branch创建新分支;点击相应分支后会弹出二级菜单:Checkout表示切换到此分支;Merge into Current 表示合并此分支到当前分支

7.1 创建dev分支

(1)点击 New Branch -> 在弹出的对话框中输入分支名dev,再按快捷键Ctrl + Shift + K 直接提交分支

(2)接下来就可以在码云平台看到新添加的dev分支了

7.2 切换分支

点击右下角的 Git dev 按钮,点击要切换的分支,在弹出的二级菜单中点击Checkout即可切换到指定分支

7.3 分支代码的合并

(1)先切换到dev分支

(2)在SpringBoot入口类中添加 test 方法

(3)按快捷键 Ctrl + K ,按照图示操作之后会弹出提交窗口,选择Push提交即可

(4)切换到master分支

(5)将dev分支合并到master分支,此时dev上修改的代码就合并到master上了

(7)按快捷键Ctrl + Shift + K 直接提交

7.4 在dev上检出一个bug分支

(1)先切换到dev分支

(2)新建一个bug分支

(3)在SpringBoot入口类中添加test1方法

(4)提交(Ctrl + K )

(5)切换到dev分支,会发现没有test1方法

(6)将bug分支合并到dev分支,再按快捷键Ctrl + Shift + K 提交

(7)最后切换到master分支,按快捷键Ctrl + Shift + K 提交

7.5 使用码云创建分支

(1)登录码云,进入要创建分支的仓库,打开分支管理

(2)创建feature分支

(3)在IDEA中点击Fetch可以更新分支到本地,更新完成后,在右下角点击git master即可看到线上新建的feature分支了

8、码云平台相关功能

可以为项目添加合作伙伴,按不同的职责分配不同的角色权限

五、项目的前期准备

1、Map和传统JavaBean技术选型

1.1 Map的优缺点

优点:

  • 灵活性强于JavaBean,易扩展,耦合度低。
  • 写起来简单,代码量少
  • MyBatis查询的返回结果本身就是Map

缺点:

  • 不能一眼看出Map中有哪些参数

1.2 JavaBean的优缺点

优点:

  • 符合Java语言面向对象设计的原则
  • 数据结构清晰,便于团队开发和后期维护

缺点:

  • 需要不断地去维护实体类

1.3 如何选型呢?

  • 团队人数少,追求开发效率,建议使用Map代替实体类
  • 项目庞大,需要持续维护,团队人数多,建议使用实体类

2、响应结果封装及全局异常处理

2.1 MVC概念

2.2 MVC流程

2.3 前后端分离

2.4 前后端分离的优缺点

优点

  • 分工更明确,提升各自领域专注度
  • 前后端代码完全分离,有利于项目维护

缺点:

  • 需要不断地维护接口文档
  • 沟通成本更高
  • 部署相比较MVC架构要复杂点

2.5 后端接口统一json数据格式

接口返回成功示例:

{"code": 1,"message": "请求成功","data": {"user_info": {"name": "张三","address": "北京市xx区"}}
}

接口请求失败示例:

{"code": 0,"message": "系统异常"
}

2.6 响应结果封装

(1)给父工程添加以下两个依赖

<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId>
</dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-core</artifactId>
</dependency>

(2)在common模块下创建utils包,用于放置系统中的工具类

(3)创建ResultCode类和Result

package com.education.common.utils;/*** http 响应状态码*  SUCCESS: 响应成功状态码*  FAIL: 响应失败状态码* @author Jason*/
public class ResultCode {public static final int SUCCESS = 1;public static final int FAIL = 0;public static final String DEFAULT_SUCCESS_MESSAGE = "操作成功";public static final String DEFAULT_FAIL_MESSAGE = "操作失败";private int code = SUCCESS;private String message;public ResultCode() {}public ResultCode(int code, String message) {this.code = code;this.message = message;}public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}
}
package com.education.common.utils;/*** 对请求结果的封装* @author Jason*/
public class Result {private Object data;private ResultCode resultCode;public Result() {}public Result(ResultCode resultCode) {this.resultCode = resultCode;}public Result(Object data) {this.resultCode = new ResultCode(ResultCode.SUCCESS, ResultCode.DEFAULT_SUCCESS_MESSAGE);this.data = data;}public Result(ResultCode resultCode,Object data) {this(resultCode);this.data = data;}/*** 响应成功* @param data 响应的数据* @return 封装的请求*/public static Result success(Object data) {return new Result(data);}/*** 响应成功* @param resultCode 响应的状态码* @param data 响应的数据* @return 封装的请求*/public static Result success(ResultCode resultCode, Object data) {return new Result(resultCode, data);}/*** 响应成功* @param resultCode 响应的状态码* @return 封装的请求*/public static Result success(ResultCode resultCode) {return new Result(resultCode);}/*** 响应失败* @param resultCode 状态码* @return 封装的请求*/public static Result fail(ResultCode resultCode) {return new Result(resultCode);}/*** 响应失败* @return 封装的请求*/public static Result fail() {return new Result(new ResultCode(ResultCode.FAIL,ResultCode.DEFAULT_FAIL_MESSAGE));}/*** 判断请求是否成功* @return 请求的状态:成功 true 失败 false*/public boolean isSuccess() {return ResultCode.SUCCESS == this.resultCode.getCode();}public <T> T getData() {return (T) data;}public void setData(Object data) {this.data = data;}public ResultCode getResultCode() {return resultCode;}public void setResultCode(ResultCode resultCode) {this.resultCode = resultCode;}
}

2.7 全局异常处理

(1)在com.education.common下创建exception

(2)在exception包下创建系统业务异常类BusinessException和全局异常处理类SystemExceptionHandler

package com.education.common.exception;import com.education.common.utils.ResultCode;/*** 系统业务异常类* @author Jason*/
public class BusinessException extends  RuntimeException {private ResultCode resultCode;public BusinessException(ResultCode resultCode) {this.resultCode = resultCode;}public BusinessException(String message) {super(message);}public BusinessException(Throwable throwable) {super(throwable);}public BusinessException(String message, Throwable throwable) {super(message, throwable);}public ResultCode getResultCode() {return resultCode;}public void setResultCode(ResultCode resultCode) {this.resultCode = resultCode;}
}
package com.education.common.exception;import com.education.common.utils.Result;
import com.education.common.utils.ResultCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;/*** 处理全局异常* @author Jason*/
@ControllerAdvice
public class SystemExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(SystemExceptionHandler.class);@ExceptionHandler(Exception.class)@ResponseBodypublic Result resolveException(Exception e) {Result result = new Result(new ResultCode(ResultCode.FAIL,ResultCode.DEFAULT_FAIL_MESSAGE));// 判断是否为业务异常if(e instanceof BusinessException) {BusinessException businessException = (BusinessException) e;if (businessException.getResultCode() != null) {result.setResultCode(businessException.getResultCode());}}logger.error("系统异常",e);return result;}
}

3、Java线程池技术

3.1 为什么要使用线程池?

减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务,降低了系统资源的消耗

3.2 Web系统中使用多线程的场景

主业务程序与子业务耦合度低:发短信或发送邮件请求第三方接口

3.3 线程池的执行原理

3.4 线程池的销毁

//等待所有正在执行的任务全试图停止所有正在执行
.shutdoen();
// 部执行完毕之后才会销毁的线程
.shutdownNow();

3.5 线程池的创建方法

Executors创建线程池

方法名 功能
newFixedThreadPool(int nThreads) 创建固定大小的线程池
newSingleThreadExcutor() 创建只有一个线程的线程池
newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行

ThreadPoolExecutor

// Java 线程池的完整构造函数
public ThreadPoolExecutor (int corePoolSize,    // 线程池长期维持的线程数,即使线程处于idle状态,也不会回收int maximumPoolSize, // 线程数的上限long keepAliveTiem, TimeUnit unit,  // 超过corePoolSize的线程的idle时长// 超过这个时间,多余的线程会被回收BlookingQueue<Runnable> workQueue,  // 任务的排队队列ThreadFactory threadFactory,        // 新线程的产生方式RejectedExecutionHandler handler)   // 拒绝策略

3.6 Spring与线程池的整合

private static final int COUNT = Runtime.getRuntime().availableProcessors(); // cpu个数
private static final int CORE_SIZE = COUNT * 2;
private static final int MAX_SIZE = COUNT * 4;@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();threadPoolTaskExecutor.setMaxPoolSize(MAX_SIZE);threadPoolTaskExecutor.setCorePoolSize(CORE_SIZE);threadPoolTaskExecutor.setQueueCapacity(20);threadPoolTaskExecutor.setKeepAliveSeconds(200);threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return threadPoolTaskExecutor;
}

4、线程池的使用

4.1 线程资源复用示例

package com.education.common;import java.util.concurrent.*;/*** Unit test for simple App.*/
public class AppTest
{private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); // 获取cpu数量private static final int CORE_SIZE = CPU_COUNT * 2;private static final int MAX_CORE_SIZE = CPU_COUNT * 4;private static final int QUEUE_SIZE = 30;static class MyThread implements Runnable {private int number;public MyThread(int number) {this.number = number;}@Overridepublic void run() {System.out.println("正在执行任务" + number + " " + Thread.currentThread());}}public static void main(String [] args) throws ExecutionException, InterruptedException {// SynchronousQueue;    是一个无界缓存等待队列,不能指定队列容量// ArrayBlockingQueue;  是一个游街缓存等待队列,可以指定队列容量ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_SIZE, // 线程池长期维持的线程数,即使线程处于idle状态,也不会回收MAX_CORE_SIZE, // 线程数的上限60L, TimeUnit.SECONDS, // 超过corePoolSize线程的idle时长,超过这个时间,多余的线程会被回收// 此处设置了队列容量,当设置了队列容量后,最大并发量不能超过队列容量和线程数的总和new LinkedBlockingQueue<>(QUEUE_SIZE));// System.out.println("并发上限:" + (MAX_CORE_SIZE + QUEUE_SIZE));for (int i = 0; i < 200; i++) {// 将任务添加到线程池Future<?> future = threadPoolExecutor.submit(new MyThread(i));if (future.get() == null) {System.out.println(Thread.currentThread() + "任务已完成");}// threadPoolExecutor.execute(new MyThread(i));}}
}

通过运行结果会发现线程资源被多次复用。

4.2 线程池的拒绝策略

(1)修改for循环体代码:

for (int i = 0; i < 200; i++) {// 将任务添加到线程池
/*Future<?> future = threadPoolExecutor.submit(new MyThread(i));if (future.get() == null) {System.out.println(Thread.currentThread() + "任务已完成");}
*/threadPoolExecutor.execute(new MyThread(i));
}

(2)重新运行,会发现报错(没报错可以增大循环次数),这是因为设置了队列容量QUEUE_SIZE,当并发量超过线程数和队列容量的总和时,就会抛出异常

(3)可以通过setRejectedExecutionHandler()方法设置拒绝策略,在for循环上方添加以下语句:

threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

(4)重新运行,会发现所有任务全部执行完成

(5)修改拒绝策略

threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

(6)重新运行,会发现有部分任务没有执行,并且也没有抛出异常

4.3 线程池在实际项目中的使用

(1) 在com.education.common.utils包下创建SpringBeanManager

package com.education.common.utils;import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;/*** bean 实例工具类* @author Jason*/
@Component
@Lazy(false)
public class SpringBeanManager implements ApplicationContextAware {private static ApplicationContext applicationContext = null;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {SpringBeanManager.applicationContext = applicationContext;}public static <T> T getBean(String name) {return (T) applicationContext.getBean(name);}public static <T> T getBean(Class<T> clazz) {return (T) applicationContext.getBean(clazz);}
}

(2) 在service模块创建com.education.service.task包,并创建TaskListener接口和TaskManager

package com.education.service.task;/*** 任务监听器* @author Jason*/
public interface TaskListener {void onMessage(TaskParam taskParam);
}
package com.education.service.task;import com.education.common.utils.SpringBeanManager;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** 任务管理器* @author Jason*/
public class TaskManager {private ThreadPoolTaskExecutor threadPoolTaskExecutor;private final Map<String, TaskListener> taskListenerMap = new ConcurrentHashMap<>();public TaskManager(ThreadPoolTaskExecutor threadPoolTaskExecutor) {this.threadPoolTaskExecutor = threadPoolTaskExecutor;this.registerTaskListener();}private void registerTaskListener() {ApplicationContext applicationContext = SpringBeanManager.getApplicationContext();String[] beanNames = applicationContext.getBeanNamesForType(TaskListener.class);for (String name: beanNames) {TaskListener taskListener = SpringBeanManager.getBean(name);taskListenerMap.put(name, taskListener);}}public void pushTask(TaskParam taskParam) {String beanName = taskParam.getTaskListenerBeanName();TaskListener taskListener = taskListenerMap.get(beanName);if (taskListener != null) {threadPoolTaskExecutor.execute(() -> {taskListener.onMessage(taskParam);});}}
}

(3)封装任务参数类TaskParam

package com.education.service.task;/*** 封装任务参数类* @author Jason*/
public class TaskParam {private String taskListenerBeanName;private final long timestamp;private Object data;public TaskParam() {this.timestamp = 0L;}public TaskParam(String taskListenerBeanName, long timestamp, Object data) {this.taskListenerBeanName = taskListenerBeanName;this.timestamp = timestamp;this.data = data;}public String getTaskListenerBeanName() {return taskListenerBeanName;}public void setTaskListenerBeanName(String taskListenerBeanName) {this.taskListenerBeanName = taskListenerBeanName;}public long getTimestamp() {return timestamp;}public <T> T getData() {return (T) data;}public void setData(Object data) {this.data = data;}
}

(4)创建配置类BeanConfig

package com.education.service.task;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.ThreadPoolExecutor;/*** 配置线程池* @author Jason*/
@Configuration
public class BeanConfig {private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();private static final int CORE_SIZE = CPU_COUNT * 2;private static final int MAX_CORE_SIZE = CPU_COUNT * 4;@Beanpublic ThreadPoolTaskExecutor threadPoolTaskExecutor() {ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();threadPoolTaskExecutor.setCorePoolSize(CORE_SIZE);threadPoolTaskExecutor.setMaxPoolSize(MAX_CORE_SIZE);threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return threadPoolTaskExecutor;}@Beanpublic TaskManager taskManager (ThreadPoolTaskExecutor threadPoolTaskExecutor) {return new TaskManager(threadPoolTaskExecutor);}
}

(5)创建TaskListener接口的实现类LogTaskListener

package com.education.service.task;import org.springframework.stereotype.Component;/*** @author Jason*/
@Component
public class LogTaskListener implements TaskListener{@Overridepublic void onMessage(TaskParam taskParam) {System.out.println("执行LogTaskListener:" + taskParam.getData());}
}

(6)在admin-api模块下com.education.admin.api.App中编写测试代码

package com.education.admin.api;import com.education.service.task.TaskManager;
import com.education.service.task.TaskParam;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;/*** @author Jason* 注解@SpringBootApplication默认扫描当前包,这样会导致无法扫描* 到com.education.service和com.education.common下的注解,* 此时就需要设置扫描包路径为com.education*/
@SpringBootApplication(scanBasePackages = "com.education")
public class App {public static void main(String[] args) {ApplicationContext applicationContext = SpringApplication.run(App.class, args);// 获取TaskManager的实例TaskManager taskManager = applicationContext.getBean(TaskManager.class);// 定义任务参数TaskParam taskParam = new TaskParam();taskParam.setData("test");// 设置监听器bean实例名称taskParam.setTaskListenerBeanName("logTaskListener");taskManager.pushTask(taskParam);System.out.println(Thread.currentThread().getName());}
}

5、整合Quartz

5.1 Quartz介绍

5.1.1 Quartz是什么?

Quartz是一个功能丰富的开源任务调度框架,它可以创建简单或者复杂的几十、几百、甚至成千上万的job。此外,quartz调度器还支持JTA事务和集群

5.1.2 Quartz与Spring Task的区别
  • Quartz默认多线程异步执行,而Spring Task默认单线程同步执行
  • Spring Task属于轻量级任务调度框架,使用更简单
  • Quartz在执行任务过程中如果抛出异常,不影响下一次任务的执行,当下一次执行时间到来时,定时器会再次执行任务。而使用Spring Task一旦某个任务在执行过程中抛出异常,则整个定时器生命周期就结束,以后永远不会再执行定时器任务。
  • Quartz每次执行都创建一个新的任务类对象,Spring Task则每次使用同一个任务类对象。
5.1.3 Quartz三要素
  • **Scheduler:**任务调度器,它是quartz的核心所在,所有的任务都是通过scheduler开始
  • **Trigger:**任务触发器
  • **JobDetail和Job:**定义任务具体执行的逻辑
5.1.4 Trigger类中常用的方法
方法名 功能
withIdentity(String name, String gtoup) 给触发器一些属性比如名字,组名
startNow() 立刻启动
withSchedule(ScheduleBuilder schedBuilder) 以某种触发器触发
usingJobData(String dataKey, Boolean value) 给具体job传递参数
5.1.5 Cron表达式

Cron表达式用于配置CronTrigger的实例,由七个子表达式组成,用来描述详细的时间信息

格式 [秒] [分] [时] [日] [月] [周] [年]
5.1.6 配置Cron表达式

Cron表达式的格式:秒 分 时 日 月 周 年(可选)

字段名 允许的值 允许的特殊字符
0—59 , - * /
0—59 , - * /
0—23 , - * /
1—31 , - * ?/LWC
1—12 或 JAN—DEC , - * /
1—7 或 SUN—SAT , - * ?/LC#
年(可选字段) empty 1970—2099, - * /
5.1.7 Cron表达式示例
表达式 含义
0 * * * ? 每一分钟触发一次
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点触发
0 0 5-15 * * ? 每天5-15点整点触发
0 0 10 * * ? 每天10点触发一次
*/5 * * * * ? 每隔5秒执行一次

5.2 Quartz定时任务实例

(1)在父类工程中添加Quartz起步依赖

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

(1)创建任务测试类

package com.education.admin.api;import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;import java.text.SimpleDateFormat;
import java.util.Date;public class TestJob implements Job {/*** 执行具体的业务逻辑* @param jobExecutionContext* @throws JobExecutionException*/@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {Date date = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println(simpleDateFormat.format(date));System.out.println("执行任务:" + TestJob.class.getSimpleName());}private static final String DEFAULT_GROUP = "default_group";public static void main(String[] args) throws SchedulerException {// 创建一个JobDetail对象JobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity(TestJob.class.getSimpleName(), DEFAULT_GROUP).build();// 创建任务触发器CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(TestJob.class.getSimpleName(), DEFAULT_GROUP).startNow().withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?")).build();// 创建任务调度器Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();scheduler.scheduleJob(jobDetail, trigger);scheduler.start();}
}

DailyTimeIntervalScheduleBuiIder

  • SimpIeScheduleBuilder:简单循环执行,设定执行次数,开始结束时间等
  • CronScheduleBuiIder:Cron表达式实现的定时执行

运行调试会发现每5秒钟会运行一次execute方法

5.3 SpringBoot整合Quartz

(1)在service模块下创建job包,再创建一个任务基类BaseJob

package com.education.service.job;import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;/*** @author Jason*/
public abstract class BaseJob extends QuartzJobBean {}

(2)创建系统任务类SystemJob

package com.education.service.job;import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;import java.text.SimpleDateFormat;
import java.util.Date;/*** @author Jason*/
public class SystemJob extends BaseJob {@Overrideprotected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {Date date = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println(simpleDateFormat.format(date));System.out.println("执行任务:" + SystemJob.class.getSimpleName());}
}

(3)创建任务配置类JobBeanConfig

package com.education.service.job;import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author Jason*/
@Configuration
public class JobBeanConfig {private static final String DEFAULT_GROUP = "default_group";@Beanpublic JobDetail systemJob() {return JobBuilder.newJob(SystemJob.class).withIdentity(SystemJob.class.getSimpleName(), DEFAULT_GROUP).build();}@Beanpublic Trigger jobTrigger() {return TriggerBuilder.newTrigger().forJob(systemJob().getKey()).withIdentity(SystemJob.class.getSimpleName(), DEFAULT_GROUP).startNow().withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?")).build();}
}

(4)修改admin-api入口类

package com.education.admin.api;import com.education.service.task.TaskManager;
import com.education.service.task.TaskParam;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;/*** @author Jason* 注解@SpringBootApplication默认扫描当前包,这样会* 导致无法扫描到com.education.service和com.education.common下的注解,* 所以需要设置扫描包路径为com.education*/
@SpringBootApplication(scanBasePackages = "com.education")
public class App {public static void main(String[] args) {SpringApplication.run(App.class, args);/*ApplicationContext applicationContext = SpringApplication.run(App.class, args);TaskManager taskManager = applicationContext.getBean(TaskManager.class);TaskParam taskParam = new TaskParam();taskParam.setData("test");taskParam.setTaskListenerBeanName("logTaskListener");taskManager.pushTask(taskParam);System.out.println(Thread.currentThread().getName());*/}
}

(5)启动main函数,会发现报错

(6)在JobBeanConfigsystemJob方法中添加storeDurably()方法

(7)重启启动main函数,会发现任务每个5秒钟执行一次

六、RBAC权限管理

1、RBAC简介

1.1 RBAC权限管理

  • 基于角色的权限访问控制(Role-Based Access Control)
  • 用户和角色关联
  • 角色关联权限

1.2 RBAC流程

1.3 什么是权限?

权限是资源的集合,主要包括菜单、页面、字段、操作功能(增删改查)等等

  • 页面权限

  • 操作权限

  • 数据权限

1.4 为什么要使用权限?

  • 使用者的角度

    在限制范围内正确的使用权力

  • 设计者角度来说

    保证系统更加安全:控制不同的角色合理的访问不同的资源

1.5 RBAC中的功能模块

2、RBAC权限系统数据库设计

2.1 系统(System)

2.1.1 系统管理员表(system_admin)
字段名 类型 KEY 默认值 / 描述
id INT PRIMARY KEY 主键
login_name VARCHAR(100) NOT NULL 登录名
name VARCHAR(100) 真实姓名
password VARCHAR(100) NOT NULL 密码
encrypt VARCHAR(100) NOT NULL MD5盐值
mobile VARCHAR(20) NOT NULL 手机号
disabled_flag TINYINT(1) DEFAULT 0 1 是 0 否
mail VARCHAR(50) 邮箱
last_login_time DATETIME 最后登录的时间
login_count INT(11) DEFAULT 0 登录次数
last_login_ip VARCHAR(100) 最后登录的IP
super_flag TINYINT(1) DEFAULT 0 是否为超级管理员
create_date DATETIME 创建时间
update_date DATETIME 更新时间
2.1.2 系统角色表(system_role)
字段名 类型 KEY 默认值 / 描述
id INT PRIMARY KEY 主键
role_name VARCHAR(100) NOT NULL 角色名称
remark VARCHAR(100) NOT NULL 备注
create_date DATETIME 创建时间
update_date DATETIME 更新时间
2.1.3 系统菜单表(system_menu)
字段名 类型 KEY 默认值 / 描述
id INT PRIMARY KEY 主键
name VARCHAR(100) NOT NULL 菜单名称
url VARCHAR(100) NOT NULL 菜单地址
permission VARCHAR(100) NOT NULL 权限标识
icon VARCHAR(100) 菜单图标
parent_id INT DEFAULT 0 父ID
sort INT DEFAULT 0 排序
type TINYINT(2) 菜单类型:0 目录 1 菜单 2 按钮
create_date DATETIME 创建时间
update_date DATETIME 更新时间
2.1.4 系统角色关联表(system_admin_role)
字段名 类型 KEY 默认值 / 描述
id INT PRIMARY KEY 主键
role_id INT NOT NULL 角色ID
admin_id INT NOT NULL 用户ID
2.1.5 角色权限关联表(system_role_menu)
字段名 类型 KEY 默认值 / 描述
id INT PRIMARY KEY 主键
role_id INT NOT NULL 角色ID
menu_id INT NOT NULL 菜单ID
2.1.6 地区表(system_region)
2.1.7 系统操作日志表(system_log)
字段名 类型 KEY 默认值 / 描述
id INT PRIMARY KEY 主键
operation_name VARCHAR(255) 操作详情
request_url VARCHAR(255) NOT NULL 请求接口
method VARCHAR(100) NOT NULL 请求方式
request_time VARCHAR(100) 接口请求时间
user_id INT(11) 用户ID(前台或后台用户)
params text 接口请求参数
exception text 接口请求异常信息
platform_type TINYINT(2) DEFAULT 1 类型(1 系统后台 2 web前端)
2.1.8 数据字典表(system_dict)

2.2 教学(education)

2.2.1 试题信息表(question_info)
2.2.1 试卷试题关联表(test_paper_question_info)
2.2.2 科目信息表(subject_info)
2.2.3 学校信息表(school_info)
2.2.4 试卷信息表(test_paper_info)
2.2.5 充值卡信息表(refillable_card)
2.2.6 用户答题记录表(user_question_answer)
2.2.7 课程信息表(course_info)
2.2.8 考试记录表(exam_info)

2.3 用户(user_info)

2.4 RBAC权限系统登录流程

2.4.1 前后端分离用户登录流程

2.5 JWP介绍及其使用

2.5.1 JWT简介

JWT:Json Web Token,是基于json的一个公开规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

2.5.2 Json Web Token的组成

(1)Header

头信息通常包含两部分,type:代表token的类型,这里使用的是JWT类型。alg:使用的Hash算法,例如HMAC SHA256或RSA。

例如:{: "alg HS256", : "typ JWT" }

(2)Payload

荷载信息,它包含一些声明Claim(实体的描述,通常是一个User信息,还包括一些其他的元数据)

(3)signature

签证信息,需要使用编码后的headerpayload以及我们提供的一个 密钥,然后使用header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过。

2.5.3 项目实践

(1)在父工程中引入依赖

<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.53</version>
</dependency>

(2)在common模块下创建包model,然后在包下创建JwtToken

package com.education.common.model;import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.util.JSONPObject;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.util.Date;/*** @author Jason*/
public class JwtToken {private final String secret;private static  final Logger logger = LoggerFactory.getLogger(JwtToken.class);public JwtToken(String secret) {if (secret == null) {throw new NullPointerException("secret value cant not ben null");}this.secret = secret;}/*** 生成JWT token* @param value* @param expirationTime* @return*/public String createToken(Object value, long expirationTime) {// 生成SecretKey对象SecretKey secretKey = this.createSecretKey();String jsonString = JSONObject.toJSONString(value);SignatureAlgorithm hs256 = SignatureAlgorithm.HS256;long nowMillis = System.currentTimeMillis();Date now = new Date();JwtBuilder jwtBuilder = Jwts.builder().setIssuedAt(now).setSubject(jsonString).signWith(hs256, secretKey);if (expirationTime > 0L) {long expMills = nowMillis + expirationTime;Date exp = new Date(expMills);// 设置token过期时间jwtBuilder.setExpiration(exp);}return jwtBuilder.compact();}private SecretKey createSecretKey() {byte[] bytes = DatatypeConverter.parseBase64Binary(secret);return new SecretKeySpec(bytes, 0, secret.length(), "AES");}public <T> T parserToken(String token, Class<T> clazz) {SecretKey secretKey = this.createSecretKey();try {Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJwt(token).getBody();String subject = claims.getSubject();return (T) JSONObject.parseObject(subject);} catch (SignatureException | MalformedJwtException e) {// token签名失败logger.error("token 签名失败", e);} catch (ExpiredJwtException e) {// token已过期logger.error("token 已过期", e);} catch (Exception e) {logger.error("token 验证异常", e);}return null;}
}

(3)编写单元测试方法

@Test
public void testToken() throws InterruptedException {JwtToken jwtToken = new JwtToken("education");Map<String, Object> params = new HashMap<>();params.put("id", 1);params.put("name", "我要自学网");String token = jwtToken.createToken(params, 2000);System.out.println(token);Thread.sleep(6000);Map<String, Object> map = jwtToken.parserToken(token, Map.class);System.out.println(map);
}

(4)测试运行,发现报错

(5)检查修正

(6)测试运行,发现能正常生成token,但解析时抛出了一个异常

(7)检查修正

(8)测试运行,运行正常

(9)修改有效时间为3秒,模拟token过期

(10)测试运行

智慧云教育平台实战项目笔记相关推荐

  1. Vue+Echarts构建可视化大数据平台实战项目(上)粒子动效,登录界面抖动,背景图轮播★

    Vue+Echarts构建可视化大数据平台实战项目(上) 前言 分享之前我们先来普及一下什么是数据可视化?数据可视化可以把数据从冰冷的数字转换成图形,揭示蕴含在数据中的规律和道理.数据可视化通俗来说就 ...

  2. UNIAPP实战项目笔记57 发送手机验证码 接入短信SDK

    UNIAPP实战项目笔记57 发送手机验证码 接入短信SDK 注册时候需要发送验证 通过验阿里云或腾讯云等短信sdk供应商 实际案例图片 后端接口文件 index.js var express = r ...

  3. 10分钟虚拟设备接入阿里云IoT平台实战

    10分钟虚拟设备接入阿里云IoT平台实战 1. 准备工作 1.1 注册阿里云账号 使用个人淘宝账号或手机号,开通阿里云账号,并通过实名认证(可以用支付宝认证) 1.2 免费开通IoT物联网套件 产品官 ...

  4. UNIAPP实战项目笔记43 购物车页面修改收货地址和修改默认地址

    UNIAPP实战项目笔记43 购物车页面修改收货地址和修改默认地址 实际案例图片 修改收货地址和修改默认地址页面布局和功能 具体内容图片自己替换哈,随便找了个图片的做示例 用到了vuex的状态机,具体 ...

  5. 计算机音乐教学软件,音乐教学软件有哪些?小知大数智慧音乐教育平台学生使用手册...

    原标题:音乐教学软件有哪些?小知大数智慧音乐教育平台学生使用手册 一.电脑端使用 第一步:登录系统小知大数智慧音乐教学平台. 选择所在学校: 使用学生账号.密码登录,即可进入平台. 第二步:学生自主学 ...

  6. UNIAPP实战项目笔记28 商品分享功能点分享按钮分享到微信好友

    UNIAPP实战项目笔记28 商品分享功能点分享按钮分享到微信好友 detail.vue 加生命周期 // 点击分享onNavigationBarButtonTap(e) {if(e.type===' ...

  7. UNIAPP实战项目笔记45 订单页面布局完成和数据渲染

    UNIAPP实战项目笔记45 订单页面布局完成和数据渲染 实际案例图片 订单页面 具体内容图片自己替换哈,随便找了个图片的做示例 具体位置见目录结构 通过 模拟数据list 来实现数据渲染 完善布局页 ...

  8. 3d饼图 vue_Vue+Echarts构建可视化大数据平台实战项目分享(附源码)(上)

    前言 分享之前我们先来普及一下什么是数据可视化?数据可视化可以把数据从冰冷的数字转换成图形,揭示蕴含在数据中的规律和道理.数据可视化通俗来说就是:数据的展示.处理和分析.目的是借助于图形化手段,清晰有 ...

  9. (上)Vue+Echarts构建可视化大数据平台实战项目分享(附源码)

    前言 分享之前我们先来普及一下什么是数据可视化?数据可视化可以把数据从冰冷的数字转换成图形,揭示蕴含在数据中的规律和道理.数据可视化通俗来说就是:数据的展示.处理和分析.目的是借助于图形化手段,清晰有 ...

  10. 在线教育数据分析实战项目案例

    数据分析实战项目案例 数据分析概览 数据分析经常遇到的问题 解决思路 常规产品指标 在线教育产品指标 市场效果广告指标 搭建指标系统流程 渠道的分类 针对搜索引擎,信息流广告如何稳定提高ROI 利用M ...

最新文章

  1. open的O_DIRECT选项
  2. R语言dplyr包将dataframe中的NA值替换(replace)为0实战:所有NA值替换(replace)为0、具体列的NA值替换(replace)为0、若干列的NA值替换(replace)为0
  3. c++ 使用vs2010调用 win32api
  4. Screened Poisson Surface Reconstruction
  5. go语言 mysql卡死,Go语言MySQL优化
  6. java实现蛇蛇大作战_蛇蛇大作战3D旋涡版
  7. 图片复印如何去除黑底_如何用AE制作可爱漂亮闪烁霓虹灯效果?只需简单几步任何人都可以...
  8. Scrum与项目管理亲体验
  9. 详解: Spark 相对于MapReduce的优势(为什么MapReduce性能不理想)
  10. ubuntu20.04 下查看(改变)本地端口开放情况,开启和关闭防火墙
  11. Python 06 编码
  12. 计算机导论论文论题,计算机导论专业论文题目 计算机导论毕业论文题目怎么定...
  13. MySQL Error 1048 奇遇记
  14. 什么是TPS,什么是QPS
  15. NPOI导出Excel 65536限制
  16. 小程序源码:uni-app云开发的网盘助手-多玩法安装简单
  17. 3dmax快速实现一个逼真地毯效果
  18. 树莓派清华镜像源“stretch”更换为“buster”,解决tensorflow、h5py安装包下载报错问题
  19. CSS Table不换行与换行的用法
  20. 日更100天(42)每天进步一点点

热门文章

  1. DDR MC DFI PHY
  2. 端午百望山爬山活动-金山词霸运营团队活动
  3. “企业级零代码黑客马拉松大赛”决赛名单公布
  4. 【环境】NVIDIA驱动安装+cuda11.0(ubuntu16.04)
  5. 完美适配Windows 11,搜狗输入法智能输入助手体验再升级
  6. 阿尔法贝塔阀原理_阿尔法(alpha)与贝塔(beta)
  7. 服务器先装系统还是先做热备,安装ibm服务器双机热备操作系统图文详细步骤.doc...
  8. HTML中span标签使用详解含多种实例(转)
  9. lisp 焊接符号标注_焊接符号标注及表示方法-详解aws焊接符号、钢结构焊接符号含义大全...
  10. 代码——几种常见的空格符号