第 1 章 SAAS-HRM系统概述与搭建环境

学习目标:

  • 理解SaaS的基本概念

  • 了解SAAS-HRM的基本需求和开发方式

  • 掌握Power Designer的用例图

  • 完成SAAS-HRM父模块及公共模块的环境搭建

  • 完成企业微服务中企业CRUD功能

1 初识SaaS

1.1 云服务的三种模式

1.1.1 IaaS(基础设施即服务)

IaaS(Infrastructure as a Service),即基础设施即服务。提供给消费者的服务是对所有计算基础设施的利用,包括处理CPU、内存、存储、网络和其它基本的计算资源,用户能够部署和运行任意软件,包括操作系统和应用程序。消费者不管理或控制任何云计算基础设施,但能控制操作系统的选择、存储空间、部署的应用,也有可能获得有限制的网络组件(例如路由器、防火墙、负载均衡器等)的控制

1.1.2 PaaS(平台即服务)

PaaS(Platform-as-a-Service),即平台即服务。提供给消费者的服务是把客户采用提供的开发语言和工具(例如Java,python, .Net等)开发的或收购的应用程序部署到供应商的云计算基础设施上去。客户不需要管理或控制底层的云基础设施,包括网络、服务器、操作系统、存储等,但客户能控制部署的应用程序,也可能控制运行应用程序的托管环境配置

1.1.3 SaaS(软件即服务)

SaaS(Software-as-a-Service),即软件即服务。提供给消费者完整的软件解决方案,你可以从软件服务商处以租用或购买等方式获取软件应用,组织用户即可通过 Internet 连接到该应用(通常使用 Web 浏览器)。所有基础结构、中间件、应用软件和应用数据都位于服务提供商的数据中心内。服务提供商负责管理硬件和软件,并根据适当的服务协议确保应用和数据的可用性和安全性。SaaS 让组织能够通过最低前期成本的应用快速建成投产。

1.1.4 区别与联系

image-20220922064758211

1.2 SaaS的概述

1.2.1 Saas详解

SaaS(Software-as-a-service)的意思是软件即服务。简单说就是在线系统模式,即软件服务商提供的软件在线服务。

1.2.2 应用领域与行业前景

SaaS软件就适用对象而言,可以划分为针对个人的与针对企业的面向个人的SaaS产品:在线文档,账务管理,文件管理,日程计划、照片管理、联系人管理,等等云类型的服务而面向企业的SaaS产品主要包括:CRM(客户关系管理)、ERP(企业资源计划管理)、线上视频或者与群组通话会议、HRM(人力资源管理)、OA(办公系统)、外勤管理、财务管理、审批管理等。

image-20220922064839124

1.2.3 Saas与传统软件对比

降低企业成本:按需购买,即租即用,无需关注软件的开发维护。

软件更新迭代快速:和传统软件相比,由于saas部署在云端,使得软件的更新迭代速度加快

支持远程办公:将数据存储到云后,用户即可通过任何连接到 Internet 的计算机或移动设备访问其信息,

2 SaaS-HRM 需求分析

2.1 什么是SaaS-HRM

SaaS-HRM是基于saas模式的人力资源管理系统。他不同于传统的人力资源软件应用,使用者只需打开浏览器即可管理上百人的薪酬、绩效、社保、入职离职。

image-20220922064857332

2.2 原型分析法

原型分析的理念是指在获取一组基本需求之后,快速地构造出一个能够反映用户需求的初始系统原型。让用户看到未来系统的概貌,以 便判断哪些功能是符合要求的,哪些方面还需要改进,然后不断地对这些需求进一步补充、细化和修改。依次类推,反复进行,直到用户满意为止并由此开发出完整 的系统。

简单的说,原型分析法就是在最短的时间内,以最直观的方式获取用户最真实的需求

http://hrm.research.itcast.cn/(废弃)

http://ihrm-java.itheima.net/

2.3 UML的用例图

2.3.1 UML统一建模语言

Unified Modeling Language (UML)又称统一建模语言或标准建模语言,是始于 1997 年一个OMG标准,它是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持,包括由需求分析到规格,到构造和配置。 面向对象的分析与设计(OOA&D,OOAD)方法的发展在 80 年代末至 90 年代中出现了一个高潮,UML是这个高潮的产物。它不仅统一了Booch、Rumbaugh和Jacobson的表示方法,而且对其作了进一步的发展,并最终统一为大众所接受的标准建模语言。UML中包含很多图形(用例图,类图,状态图等等),其中用例图是最能体现系统结构的图形

2.3.2 用例图

用例图(use case)主要用来描述用户与用例之间的关联关系。说明的是谁要使用系统,以及他们使用该系统可以做些什么。一个用例图包含了多个模型元素,如系统、参与者和用例,并且显示这些元素之间的各种关系,如泛化、关联和依赖。它展示了一个外部用户能够观察到的系统功能模型图。

2.3.3 需求分析软件

Power Designer 是Sybase公司的CASE工具集,使用它可以方便地对管理信息系统进行分析设计,他几乎包括了数据库模型设计的全过程。利用Power Designer可以制作数据流程图、概念数据模型、物理数据模型,还可以为数据仓库制作结构模型,也能对团队设计模型进行控制。

( 1 )下载安装

使用第一天资料中准备好的安装包安装Power Designer,安装过程略

( 2 )使用Power Designer绘制用例图

绘制步骤:

文件=>建立新模型=>选择Modeltypes=>Use Case

image-20220922065038975

基本用例图:

image-20220922065045247

3 系统设计

3.1 开发方式

SaaS-IHRM系统采用前后端分离的开发方式。

image-20220922065420583

后端给前端提供数据,前端负责HTML渲染(可以在服务器渲染,也可以在浏览器渲染)和用户交互。双方通过文档的形式规范接口内容

3.2 技术架构

( 1 ) 前端技术栈

以Node.js为核心的Vue.js前端技术生态架构

( 2 ) 后端技术栈

SpringBoot+SpringCloud+SpringMVC+SpringData(Spring全家桶)

3.3 系统结构

image-20220922065438133

3.4 API文档

课程提供了前后端开发接口文档(采用Swagger语言进行编写),并与Ngin进行了整合。双击Nginx执行文件启动后,在地址栏输入http://localhost:801 即可访问API文档

4 工程搭建

4.1 前置知识点的说明

Saas-HRM系统后端采用

SpringBoot+SpringCloud+SpringMVC+SpringData

Saas-HRM系统前端采用

基于nodejs的vue框架完成编写使用element-ui组件库快速开发前端界面

学员应对以上前后端技术有初步的了解

4.2 开发环境要求

JDK1。

数据库mysql 5.

开发工具 idea 2017.1.

maven版本3.3.

4.2.1 lombok 插件

lombok是一款可以精减java代码、提升开发人员生产效率的辅助工具,利用注解在编译期自动生成setter/getter/toString()/constructor之类的代码

( 1 ) idea中安装插件

image-20220922065502350

( 2 ) 在pom文件中添加插件的依赖

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency

( 3 )常见注解

  • @Data 注解在类上;提供类所有属性的 getting 和 setting 方法,此外还提供了equals、canEqual、hashCode、toString 方法

  • @Setter :注解在属性上;为属性提供 setting 方法

  • @Setter :注解在属性上;为属性提供 getting 方法

  • @NoArgsConstructor :注解在类上;为类提供一个无参的构造方法

  • @AllArgsConstructor :注解在类上;为类提供一个全参的构造方法

4.3 构建父工程

在IDEA中创建父工程ihrm_parent并导入相应的坐标如下:

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
<packaging>pom</packaging>
<name>ihrm_parent</name>
<description>IHRM-黑马程序员</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<fastjson.version>1.2.47</fastjson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<!--编译插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!--单元测试插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>

4.4 构建公共子模块

4.4.1 构建公共子模块ihrm-common

image-20220922065829293

4.4.2 创建返回结果实体类

( 1 )新建com.ihrm.common.entity包,包下创建类Result,用于控制器类返回结果

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 com.ihrm.common.entity;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
//非空数据不显示
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
private boolean success;//是否成功
private Integer code;// 返回码
private String message;//返回信息
private Object data;// 返回数据
public Result(ResultCode code) {
this.success = code.success;
this.code = code.code;
this.message = code.message;
}
public Result(ResultCode code,Object data) {
this.success = code.success;
this.code = code.code;
this.message = code.message;
this.data = data;
}
public Result(Integer code,String message,boolean success) {
this.code = code;
this.message = message;
this.success = success;
}
public static Result SUCCESS(){
return new Result(ResultCode.SUCCESS);
}
public static Result ERROR(){
return new Result(ResultCode.SERVER_ERROR);
}
public static Result FAIL(){
return new Result(ResultCode.FAIL);
}
}

( 2 )创建类PageResult ,用于返回分页结果

1
2
3
4
5
6
7
8
9
10
11
12
package com.ihrm.common.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult<T> {
private Long total;
private List<T> rows;
}

4.4.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
public enum ResultCode {
SUCCESS(true,10000,"操作成功!"),
//---系统错误返回码-----
FAIL(false,10001,"操作失败"),
UNAUTHENTICATED(false,10002,"您还未登录"),
UNAUTHORISE(false,10003,"权限不足"),
SERVER_ERROR(false,99999,"抱歉,系统繁忙,请稍后重试!");
//---用户操作返回码----
//---企业操作返回码----
//---权限操作返回码----
//---其他操作返回码----
//操作是否成功
boolean success;
//操作代码
int code;
//提示信息
String message;
ResultCode(boolean success,int code, String message){
this.success = success;
this.code = code;
this.message = message;
}
public boolean success() {
return success;
}
public int code() {
return code;
}
public String message() {
return message;
}
}

4.4.4 分布式ID生成器

目前微服务架构盛行,在分布式系统中的操作中都会有一些全局性ID的需求,所以我们不能使用数据库本身的自增功能来产生主键值,只能由程序来生成唯一的主键值。我们采用的是开源的twitter( 非官方中文惯称:推特.是国外的一个网站,是一个社交网络及微博客服务) 的snowflake (雪花)算法。

image-20220922070107097

各个段解析:

image-20220922070128089

默认情况下41bit的时间戳可以支持该算法使用到 2082 年,10bit的工作机器id可以支持 1024 台机器,序列号支持 1 毫秒产生 4096 个自增序列id. SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生 26 万ID左右

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
//雪花算法代码实现
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private final static long twepoch = 1288834974657L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒内自增位
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits +
datacenterIdBits;
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/* 上次生产id时间戳 */
private static long lastTimestamp = -1L;
// 0,并发控制
private long sequence = 0L;
private final long workerId;
// 数据标识id部分
private final long datacenterId;
public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作机器ID
* @param datacenterId
* 序列号
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
this.workerId= workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
*
* @return
*/
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
return nextId;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* <p>
* 获取 maxWorkerId
* </p>
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuffer mpid = new StringBuffer();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
/*
* GET jvmPid
*/
mpid.append(name.split("@")[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
/**
* <p>
* 数据标识id部分
* </p>
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
System.out.println(" getDatacenterId: " + e.getMessage());
}
return id;
}
}

4.5 搭建公共的实体类模块

( 1 )构建公共子模块ihrm_common_model

image-20220922070651784

( 2 )引入坐标

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.ihrm</groupId>
<artifactId>ihrm_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

5 企业微服务-企业CRUD

5.1 模块搭建

( 1 )搭建企业微服务模块ihrm_company, 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-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.ihrm</groupId>
<artifactId>ihrm_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

( 2 )添加配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9001
spring:
application:
name: ihrm-company #指定服务名
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/ihrm?useUnicode=true&characterEncoding=utf
username: root
password: 111111
jpa:
database: MySQL
show-sql: true
open-in-view: true

( 3 )配置启动类

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication(scanBasePackages = "com.ihrm")
@EntityScan("com.ihrm")
public class CompanyApplication {
public static void main(String[] args) {
SpringApplication.run(CompanyApplication.class, args);
}
@Bean
public IdWorker idWorkker() {
return new IdWorker(1, 1);
}
}

5.2 企业管理-CRUD

5.2.1 表结构分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE `co_company` (
`id` varchar(40) NOT NULL COMMENT 'ID',
`name` varchar(255) NOT NULL COMMENT '公司名称',
`manager_id` varchar(255) NOT NULL COMMENT '企业登录账号ID',
`version` varchar(255) DEFAULT NULL COMMENT '当前版本',
`renewal_date` datetime DEFAULT NULL COMMENT '续期时间',
`expiration_date` datetime DEFAULT NULL COMMENT '到期时间',
`company_area` varchar(255) DEFAULT NULL COMMENT '公司地区',
`company_address` text COMMENT '公司地址',
`business_license_id` varchar(255) DEFAULT NULL COMMENT '营业执照-图片ID',
`legal_representative` varchar(255) DEFAULT NULL COMMENT '法人代表',
`company_phone` varchar(255) DEFAULT NULL COMMENT '公司电话',
`mailbox` varchar(255) DEFAULT NULL COMMENT '邮箱',
`company_size` varchar(255) DEFAULT NULL COMMENT '公司规模',
`industry` varchar(255) DEFAULT NULL COMMENT '所属行业',
`remarks` text COMMENT '备注',
`audit_state` varchar(255) DEFAULT NULL COMMENT '审核状态',
`state` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态',
`balance` double NOT NULL COMMENT '当前余额',
`create_time` datetime NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5.2.2 完成企业增删改查操作

( 1 )实体类(domain)

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
@Entity
@Table(name = "co_company")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Company implements Serializable {
private static final long serialVersionUID = 594829320797158219L;
//ID
@Id
private String id;
/**
* 公司名称
*/
private String name;
/**
* 企业登录账号ID
*/
private String managerId;
/**
* 当前版本
*/
private String version;
/**
* 续期时间
*/
private Date renewalDate;
/**
* 到期时间
*/
private Date expirationDate;
/**
* 公司地区
*/
private String companyArea;
/**
* 公司地址
*/
private String companyAddress;
/**
* 营业执照-图片ID
*/
private String businessLicenseId;
/**
* 法人代表
    */
private String legalRepresentative;
/**
* 公司电话
*/
private String companyPhone;
/**
* 邮箱
*/
private String mailbox;
/**
* 公司规模
*/
private String companySize;
/**
* 所属行业
*/
private String industry;
/**
* 备注
*/
private String remarks;
/**
* 审核状态
*/
private String auditState;
/**
* 状态
*/
private Integer state;
/**
* 当前余额
*/
private Double balance;
/**
* 创建时间
*/
private Date createTime;
}

( 2 )持久层(dao)

JpaRepository提供了基本的增删改查 JpaSpecificationExecutor用于做复杂的条件查询

1
2
3
4
5
/**
*企业数据访问接口
*/
public interface CompanyDao extends JpaRepository<Company, String>, JpaSpecificationExecutor<Company> {
}

( 3 )业务逻辑层(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
@Service
public class CompanyService {
@Autowired
private CompanyDao companyDao;
@Autowired
private IdWorker idWorker;
/**
* 添加企业
*
* @param company 企业信息
*/
public Company add(Company company) {
company.setId(idWorker.nextId() + "");
company.setCreateTime(new Date());
company.setState(1); //启用
company.setAuditState("0"); //待审核
company.setBalance(0d);
return companyDao.save(company);
}
public Company update(Company company) {
return companyDao.save(company);
}
public Company findById(String id) {
return companyDao.findById(id).get();
}
public void deleteById(String id) {
companyDao.deleteById(id);
}
public List<Company> findAll() {
return companyDao.findAll();
}
}

( 4 )控制器

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
@RestController
@RequestMapping("/company")
public class CompanyController{
@Autowired
private CompanyService companyService;
/**
* 添加企业
*/
@RequestMapping(value = "", method = RequestMethod.POST)
public Result add(@RequestBody Company company) throws Exception {
companyService.add(company);
return Result.SUCCESS();
}
/**
* 根据id更新企业信息
*/
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public Result update(@PathVariable(name = "id") String id, @RequestBody Company
company) throws Exception {
Company one = companyService.findById(id);
one.setName(company.getName());
one.setRemarks(company.getRemarks());
one.setState(company.getState());
one.setAuditState(company.getAuditState());
companyService.update(company);
return Result.SUCCESS();
}
/**
* 根据id删除企业信息
*/
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public Result delete(@PathVariable(name = "id") String id) throws Exception {
companyService.deleteById(id);
return Result.SUCCESS();
}
/**
* 根据ID获取公司信息
*/
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public Result findById(@PathVariable(name = "id") String id) throws Exception {
Company company = companyService.findById(id);
return new Result(ResultCode.SUCCESS);
}
/**
* 获取企业列表
*/
@RequestMapping(value = "", method = RequestMethod.GET)
public Result findAll() throws Exception {
List<Company> companyList = companyService.findAll();
return new Result(ResultCode.SUCCESS);
}
}

5.2.3 测试

( 1 ) 测试工具postman

Postman提供功能强大的Web API & HTTP 请求调试。软件功能非常强大,界面简洁明晰、操作方便快捷,设计得很人性化,能够发送任何类型的HTTP 请求 (GET, HEAD, POST, PUT..),附带任何数量的参数。

使用资料中提供的postman安装包进行安装,注册成功之后即可使用

( 2 ) 使用postman测试企业接口

image-20220922071247657

5.3 公共异常处理

为了使我们的代码更容易维护,同时给用户最好的用户体验,有必要对系统中可能出现的异常进行处理。spring提供

了@ControllerAdvice注解和@ExceptionHandler可以很好的在控制层对异常进行统一处理

( 1 )添加自定义的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.ihrm.common.exception;
import com.ihrm.common.entity.ResultCode;
import lombok.Getter;
@Getter
public class CommonException extends RuntimeException {
private static final long serialVersionUID = 1L;
private ResultCode code = ResultCode.SERVER_ERROR;
public CommonException(){}
public CommonException(ResultCode resultCode) {
super(resultCode.message());
this.code = resultCode;
}
}

( 2 )配置公共异常处理

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
package com.ihrm.common.exception;
import com.alibaba.fastjson.JSON;
import com.ihrm.common.entity.Result;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 全局异常处理
*/
@ControllerAdvice
public class BaseExceptionHandler {
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Result error(HttpServletRequest request, HttpServletResponse response,
Exception e) throws IOException {
e.printStackTrace();
if (e.getClass() == CommonException.class) {
CommonException ce = (CommonException) e;
return new Result(ce.getCode());
} else {
return Result.ERROR();
}
}
}

5.4 跨域处理

跨域是什么?浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 。我们是采用前后端分离开发的,也是前后端分离部署的,必然会存在跨域问题。 怎么解决跨域?很简单,只需要在controller类上添加注解@CrossOrigin 即可!这个注解其实是CORS的实现。 CORS(Cross-Origin ResourceSharing, 跨源资源共享)是W3C出的一个标准,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。因此,要想实现CORS进行跨域,需要服务器进行一些设置,同时前端也需要做一些配置和分析。本文简单的对服务端的配置和前端的一些设置进行分析。

第 2 章 数据库设计与前端框架

学习目标:

  • 理解多租户的数据库设计方案

  • 熟练使用PowerDesigner构建数据库模型

  • 理解前端工程的基本架构和执行流程

  • 完成前端工程企业模块开发

1 多租户SaaS平台的数据库方案

1.1 多租户是什么

多租户技术(Multi-TenancyTechnology)又称多重租赁技术:是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离

1.2 需求分析

传统软件模式,指将软件产品进行买卖,是一种单纯的买卖关系,客户通过买断的方式获取软件的使用权,软件的源码属于客户所有,因此传统软件是部署到企业内部,不同的企业各自部署一套自己的软件系统

Saas模式,指服务提供商提供的一种软件服务,应用统一部署到服务提供商的服务器上,客户可以根据自己的实际需求按需付费。用户购买基于WEB的软件,而不是将软件安装在自己的电脑上,用户也无需对软件进行定期的维护与管理

image-20220922071957330

在SaaS平台里需要使用共用的数据中心以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍可以保障客户的数据正常使用。由此带来了新的挑战,就是如何对应用数据进行设计,以支持多租户,而这种设计的思路,是要在数据的共享、安全隔离和性能间取得平衡。

1.3 多租户的数据库方案分析

目前基于多租户的数据库设计方案通常有如下三种:

  • 独立数据库

  • 共享数据库、独立 Schema

  • 共享数据库、共享数据表

1.3.1 独立数据库

独立数据库:每个租户一个数据库。

  • 优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。

  • 缺点: 增多了数据库的安装数量,随之带来维护成本和购置成本的增加

这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。由此可见此方案用户数据隔离级别最高,安全性最好,但是成本较高

1.3.2 共享数据库、独立 Schema

( 1 ) 什么是Schema

oracle数据库:在oracle中一个数据库可以具有多个用户,那么一个用户一般对应一个Schema,表都是建立在Schema中的,(可以简单的理解:在oracle中一个用户一套数据库表)

image-20220922072040695

mysql数据库:mysql数据中的schema比较特殊,并不是数据库的下一级,而是等同于数据库。比如执行create schema test 和执行create database test效果是一模一样的共享数据库、独立 Schema:即多个或所有的租户使用同一个数据库服务(如常见的ORACLE或MYSQL数据库),但是每个租户一个Schema。

image-20220922072048084

  • 优点: 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。

  • 缺点: 如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据; 如果需要跨租户统计数据,存在一定困难。

这种方案是方案一的变种。只需要安装一份数据库服务,通过不同的Schema对不同租户的数据进行隔离。由于数据库服务是共享的,所以成本相对低廉。

1.3.3 共享数据库、共享数据表

共享数据库、共享数据表:即租户共享同一个Database,同一套数据库表(所有租户的数据都存放在一个数据库的同一套表中)。在表中增加租户ID等租户标志字段,表明该记录是属于哪个租户的。

  • 优点:所有租户使用同一套数据库,所以成本低廉。

  • 缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量,数据备份和恢复最困难。

这种方案和基于传统应用的数据库设计并没有任何区别,但是由于所有租户使用相同的数据库表,所以需要做好对每个租户数据的隔离安全性处理,这就增加了系统设计和数据管理方面的复杂程度。

image-20220922072059032

1.4 SAAS-HRM数据库设计

在SAAS-HRM平台中,分为了试用版和正式版。处于教学的目的,试用版采用共享数据库、共享数据表的方式设计。正式版采用基于mysql的共享数据库、独立 Schema设计(后续课程)。

2 数据库设计与建模

2.1 数据库设计的三范式

三范式:

1.第一范式(1NF):确保每一列的原子性(做到每列不可拆分)

2.第二范式(2NF):在第一范式的基础上,非主字段必须依赖于主字段(一个表只做一件事)

3.第三范式(3NF):在第二范式的基础上,消除传递依赖

反三范式:

反三范式是基于第三范式所调整的,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。

2.2 数据库建模

了解了数据的设计思想,那对于数据库表的表设计应该怎么做呢?答案是数据库建模

数据库建模:在设计数据库时,对现实世界进行分析、抽象、并从中找出内在联系,进而确定数据库的结构。它主要包括两部分内容:确定最基本的数据结构;对约束建模。

2.2.1 建模工具

对于数据模型的建模,最有名的要数PowerDesigner,PowerDesigner是在中国软件公司中非常有名的,其易用性、功能、对流行技术框架的支持、以及它的模型库的管理理念,都深受设计师们喜欢。他的优势在于:不用在使用create table等语句创建表结构,数据库设计人员只关注如何进行数据建模即可,将来的数据库语句,可以自动生成

2.2.2 使用pd建模

  1. 选择新建数据库模型 打开PowerDesigner,文件->建立新模型->model types(选择类型)->Physical Data Model(物理模型)

image-20220922072124025

  1. 控制面板

image-20220922072129699

  1. 创建数据库表

点即面板按钮中的创建数据库按钮创建数据库模型

image-20220922072141532

切换columns标签,可以对表中的所有字段进行配置

image-20220922072146158

如果基于传统的数据库设计中存在外键则可以使用面版中的Reference配置多个表之间的关联关系,效果如下图

image-20220922072151937

  1. 导出sql

菜单->数据库(database)->生成数据库表结构(Generate Database)

3 前端框架

3.1 脚手架工程

此项目采用目前比较流行的前后端分离的方式进行开发。前端是在传智播客研究院开源的前端框架(黑马Admin商用后台模板)的基础上进行的开发。

官网上提供了非常基础的脚手架,如果我们使用官网的脚手架需要自己写很多代码比如登陆界面、主界面菜单样式等内容。 课程已经提供了功能完整的脚手架,我们可以拿过来在此基础上开发,这样可以极大节省我们开发的时间。

技术栈

  • vue 2.5++

  • elementUI 2.2.

  • vuex

  • axios

  • vue-router

  • vue-i18n

前端环境

  • node 8.++

  • npm 5.++

3.2 启动与安装

官网上提供了非常基础的脚手架,如果我们使用官网的脚手架需要自己写很多代码比如登陆界面、主界面菜单样式

等内容。 课程已经提供了功能完整的脚手架,我们可以拿过来在此基础上开发,这样可以极大节省我们开发的时

间。

( 1 )解压提供的资源包

( 2 )在命令提示符进入该目录,输入命令:

1
cnpm install

通过淘宝镜像下载安装所有的依赖,几分钟后下载完成

如果没有安装淘宝镜像,请使用npm install

( 3 )关闭语法检查

打开config/index.js将useEslint的值改为false。

1
useEslint: false,

此配置作用: 是否开启语法检查,语法检查是通过ESLint 来实现的。我们现在科普一下,什么是ESLint : ESLint是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。如果我们开启了Eslint , 也就意味着要接受它非常苛刻的语法检查,包括空格不能少些或多些,必须单引不能双引,语句后不可以写分号等等,这些规则其实是可以设置的。我们作为前端的初学者,最好先关闭这种校验,否则会浪费很多精力在语法的规范性上。如果以后做真正的企业级开发,建议开启

( 4 )输入命令:

1
npm run dev

3.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
├── assets | 资源
├── build | webpack编译配置
├── config | 全局变量
├── src | 源码
│ ├── api | 数据请求
│ ├── assets | 资源
│ ├── components | 组件
│ ├── mixins | mixins
│ ├── filters | vue filter
│ ├── icons | 图标
│ ├── lang | 多语言
│ ├── router | 路由
│ ├── store | 数据
│ ├── styles | 样式
│ ├── utils | 工具函数库
│ ├── module-dashboard | 框架程序
│ │ ├── assets
│ │ ├── components
│ │ ├── pages
│ │ ├── router
│ │ └── store
│ ├── module-example | 示例程序
│ │ ├── assets
│ │ ├── components
│ │ ├── pages
│ │ ├── router
│ │ └── store
│ ├── App.vue | app
│ ├── main.js | 主引导
│ └── errorLog.js | vue全局错误捕捉
├── dist | 编译发布目录
├── README.md
├── index.html | 页面模板
├── package.json | npn包配置
├── static
└── test | 测试
├── e2e
└── unit

3.4 执行流程分析

3.4.1 路由和菜单

路由和菜单是组织起一个后台应用的关键骨架。本项目侧边栏和路由是绑定在一起的,所以你只有在@/router/index.js 下面配置对应的路由,侧边栏就能动态的生成了。大大减轻了手动编辑侧边栏的工作量。当然这样就需要在配置路由的时候遵循很多的约定这里的路由分为两种,constantRouterMap 和 asyncRouterMap。

  • constantRouterMap 代通用页面。

  • asyncRouterMap 代表那些业务中通过 addRouters 动态添加的页面。

image-20220922085343647

3.4.2 前端数据交互

一个完整的前端 UI 交互到服务端处理流程是这样的:

  1. UI 组件交互操作;

  2. 调用统一管理的 api service 请求函数;

  3. 使用封装的 request.js 发送请求;

  4. 获取服务端返回;

  5. 更新 data;

从上面的流程可以看出,为了方便管理维护,统一的请求处理都放在 src/api 文件夹中,并且一般按照 model纬度进行拆分文件

1
2
3
4
5
6
api/
frame.js
menus.js
users.js
permissions.js
...

其中,src/utils/request.js 是基于 axios 的封装,便于统一处理 POST,GET 等请求参数,请求头,以及错误提示信息等。具体可以参看 request.js。 它封装了全局 request拦截器、respone拦截器、统一的错误处理、统一做了超时,baseURL设置等

4 企业管理

4.1 需求分析

在通用页面配置企业管理模块,完成企业的基本操作

4.2 搭建环境

4.2.1 新增模块

( 1 )手动创建

方式一:在src目录下创建文件夹,命名规则:module-模块名称()

在文件夹下按照指定的结构配置assets,components,pages,router,store等文件

( 2 )使用命令自动创建

安装命令行工具

1
npm install -g itheima-cli

执行命令

1
2
3
4
5
6
7
8
9
10
11
12
itheima moduleAdd saas-clients
`saas-clients` 是新模块的名字
自动创建这些目录和文件
│ ├── module-saas-clients | saas-clients模块主目录
│ │ ├── assets | 资源
│ │ ├── components | 组件
│ │ ├── pages | 页面
│ │ │ └── index.vue | 示例
│ │ ├── router | 路由
│ │ │ └── index.js | 示例
│ │ └── store | 数据
│ │ └── app.js | 示例

每个模块所有的素材、页面、组件、路由、数据,都是独立的,方便大型项目管理,

在实际项目中会有很多子业务项目,它们之间的关系是平行的、低耦合、互不依赖。

4.2.2 构造模拟数据

( 1 )在/src/mock中添加模拟数据company.js

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
import Mock from 'mockjs'
import { param2Obj } from '@/utils'
const List = []
const count = 100
for (let i = 0 ; i < 3 ; i++) {
let data = {
id: "1"+i,
name: "企业"+i,
managerId: "string",
version: "试用版v1.0",
renewalDate: "2018-01-01",
expirationDate: "2019-01-01",
companyArea: "string",
companyAddress: "string",
businessLicenseId: "string",
legalRepresentative: "string",
companyPhone: "13800138000",
mailbox: "string",
companySize: "string",
industry: "string",
remarks: "string",
auditState: "string",
state: "1",
balance: "string",
createTime: "string"
}
List.push(data)
}
export default {
list: () => {
return {
code: 10000 ,
success: true,
message: "查询成功",
data:List
}
},
sassDetail:() => {
return {
code: 10000 ,
success: true,
message: "查询成功",
data:{
id: "10001",
name: "测试企业",
managerId: "string",
version: "试用版v1.0",
renewalDate: "2018-01-01",
expirationDate: "2019-01-01",
companyArea: "string",
companyAddress: "string",
businessLicenseId: "string",
legalRepresentative: "string",
companyPhone: "13800138000",
mailbox: "string",
companySize: "string",
industry: "string",
remarks: "string",
auditState: "string",
state: "1",
balance: "string",
createTime: "string"
}
}
}
}

( 2 )配置模拟API接口拦截规则

在/src/mock/index.js中配置模拟数据接口拦截规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Mock from 'mockjs'
import TableAPI from './table'
import ProfileAPI from './profile'
import LoginAPI from './login'
import CompanyAPI from './company'
Mock.setup({
//timeout: '1000'
})
//如果发送请求的api路径匹配,拦截
//第一个参数匹配的请求api路径,第二个参数匹配请求的方式,第三个参数相应数据如何替换
Mock.mock(/\/table\/list\.*/, 'get', TableAPI.list)
//获取用户信息
Mock.mock(/\/frame\/profile/, 'post', ProfileAPI.profile)
Mock.mock(/\/frame\/login/, 'post', LoginAPI.login)
//配置模拟数据接口
// /company/12
Mock.mock(/\/company\/+/, 'get', CompanyAPI.sassDetail)//根据id查询
Mock.mock(/\/company/, 'get', CompanyAPI.list) //访问企业列表

4.2.3 注册模块

编辑 src/main.js

1
2
3
4
5
6
7
...
// 注册 - 业务模块
import dashboard from '@/module-dashboard/' // 面板
import saasClients from '@/module-saas-clients/' //刚新添加的 企业管理
Vue.use(dashboard, store)
Vue.use(saasClients, store)
...

4.2.4 配置路由菜单

打开刚才自动创建的 /src/module-saas-clients/router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Layout from '@/module-dashboard/pages/layout'
const _import = require('@/router/import_' + process.env.NODE_ENV)
export default [
{
path: '/saas-clients',
component: Layout,
redirect: 'noredirect',
name: 'saas-clients',
meta: {
title: 'SaaS企业管理',
icon: 'international'
},
root: true,
children: [
{
path: 'index',
name: 'saas-clients-index',
component: _import('saas-clients/pages/index'),
meta: {title: 'SaaS企业', icon: 'international', noCache: true}
}
]
}
]

4.2.5 编写业务页面

创建 /src/module-saas-clients/pages/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="dashboard-container">
saas企业管理
</div>
</template>
<script>
export default {
name: 'saasClintList',
components: {},
data() {
return {
}
},
computed: {
},
created() {
}
}
</script>

注意文件名 驼峰格式 首字小写

页面请放在目录 /src/module-saas-clients/pages/

组件请放在目录 /src/module-saas-clients/components/

页面路由请修改 /src/module-saas-clients/router/index.js

4.3 企业操作

4.3.1 创建api

在api/base目录下创建企业数据交互的API(saasClient.js)

1
2
3
import {createAPI, createFormAPI} from '@/utils/request'
export const list = data => createAPI('/company', 'get', data)
export const detail = data => createAPI(`/company/${data.id}`, 'get', data)

4.3.1 企业列表

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
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card shadow="never">
<!--elementui的table组件
data:数据模型
-->
<el-table :data="dataList" border style="width: 100%">
<!--el-table-column : 构造表格中的每一列
prop: 数组中每个元素对象的属性名
-->
<el-table-column fixed type="index" label="序号" width="50"></el-table-
column>
<el-table-column fixed prop="name" label="企业名称" width="200"></el-table-
column>
<el-table-column fixed prop="version" label="版本" width="150"></el-table-
column>
<el-table-column fixed prop="companyphone" label="联系电话" width="150">
</el-table-column>
<el-table-column fixed prop="expirationDate" label="截至时间" width="150">
</el-table-column>
<el-table-column fixed prop="state" label="状态" width="150">
<!--scope:传递当前行的所有数据 -->
<template slot-scope="scope">
<!--开关组件
active-value:激活的数据值
active-color:激活的颜色
inactive-value:未激活
inactive-color:未激活的颜色
-->
<el-switch
v-model="scope.row.state"
inactive-value="0"
active-value="1"
disabled
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="150">
<template slot-scope="scope">
<router-link :to="'/saas-clients/details/'+scope.row.id">查看</router-
link>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<script>
import {list} from '@/api/base/saasClient'
export default {
name: 'saas-clients-index',
data () {
return {
dataList:[]
}
},
methods: {
getList() {
//调用API发起请求
//res=响应数据
list().then(res => {
this.dataList = res.data.data
})
}
},
// 创建完毕状态
created() {
this.getList()
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.alert {
margin: 10 px 0 px 0 px 0 px;
}
.pagination {
margin-top: 10 px;
text-align: right;
}
</style>

4.3.2 企业详情

( 1 )配置路由

在/src/module-saas-clients/router/index.js添加新的子路由配置

1
2
3
4
5
6
{
path: 'details/:id',
name: 'saas-clients-details',
component: _import('saas-clients/pages/sass-details'),
meta: {title: 'SaaS企业详情', icon: 'international', noCache: false}
}

( 2 )定义公共组件

在/src/module-saas-clients/components/下创建公共的组件页面enterprise-info.vue

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
<template>
<div class="boxInfo">
<!-- 表单内容 -->
<div class="formInfo">
<div>
<div class="boxMain">
<el-form ref="form" :model="formData" label-width="215px" labelposition="right">
<el-form-item class="formInfo" label="公司名称:">
<el-input v-model="formData.name" class="inputW" disabled></el-input>
</el-form-item>
<el-form-item class="formInfo" label="公司地区:">
<el-input v-model="formData.companyArea" class="inputW" disabled></elinput>
</el-form-item>
<el-form-item class="formInfo" label="公司地址:">
<el-input v-model="formData.companyAddress" class="inputW" disabled> </el-input>
</el-form-item>
<el-form-item class="formInfo" label="审核状态:">
<el-input v-model="formData.auditState" class="inputW" disabled></elinput>
</el-form-item>
<el-form-item class="formInfo" label="营业执照:">
<span v-for="item in fileList" :key='item.id' class="fileImg">
<img :src="item.url">
</span>
</el-form-item>
<el-form-item class="formInfo" label="法人代表:">
<el-input v-model="formData.legalRepresentative" class="inputW"
disabled></el-input>
</el-form-item>
<el-form-item class="formInfo" label="公司电话:">
<el-input v-model="formData.companyPhone" class="inputW" disabled></elinput>
</el-form-item>
<el-form-item class="formInfo" label="邮箱:">
<el-input v-model="formData.mailbox" class="inputW" disabled></elinput>
</el-form-item>
<el-form-item class="formInfo" label="公司规模:">
<el-input v-model="formData.companySize" class="inputW" disabled></elinput>
</el-form-item>
<el-form-item class="formInfo" label="所属行业:">
<el-input v-model="formData.industry" class="inputW" disabled></elinput>
</el-form-item>
<el-form-item class="formInfo" label="备注:">
<el-input type="textarea" v-model="formData.remarks" class="inputW"> </el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSub('1')">审核</el-button>
<el-button @click="handleSub('2')">拒绝</el-button>

</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { auditDetail } from '@/api/base/sassClients'
import { imgDownload } from '@/api/base/baseApi'
var _this = null
export default {
name: 'userInfo',
components: {},
props: ['formData'],
data() {
return {
fileList: []
}
},
methods: {
// 业务方法
// 界面交互
handleSub(state) {
auditDetail({
id: this.formData.id,
remarks: this.formData.remarks,
state: state
}).then(() => {
if (state === '1') {
this.$message.success('恭喜你,审核成功!')
}
if (state === '2') {
this.$message.success('已拒绝审核!')
}
this.$emit('getObjInfo', this.formData)
})
},
// 图片 blob 流转化为可用 src
imgHandle(obj) {
return window.URL.createObjectURL(obj)
},
// 图片下载
fillDownload(fid) {
}
},
// 挂载结束
mounted: function() {},
// 创建完毕状态
created: function() {
_this = this
},
// 组件更新
updated: function() {
// this.imgDownInfo()
if (
this.formData.businessLicense !== null
) {
this.fillDownload(this.formData.businessLicense)
}
}
}</script>
<style rel="stylesheet/scss" lang="scss"> </style>
<style rel="stylesheet/scss" lang="scss" scoped> .fileImg{
img{
width:20%;
}
}
</style>

( 3 )完成详情展示

在在/src/module-saas-clients/pages/下创建企业详情视图details.vue

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
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card shadow="never">
<el-tabs v-model="activeName">
<el-tab-pane label="企业信息" name="first">
<!--form表单
model : 双向绑定的数据对象
-->
<el-form ref="form" :model="formData" label-width="200px">
<el-form-item label="企业名称" >
<el-input v-model="formData.name" style="width:250px" disabled> </el-input>
</el-form-item>
<el-form-item label="公司地址">
<el-input v-model="formData.companyAddress" style="width:250px"
disabled></el-input>
</el-form-item>
<el-form-item label="公司电话">
<el-input v-model="formData.companyPhone" style="width:250px"
disabled></el-input>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="formData.mailbox" style="width:250px"
disabled></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="formData.remark" style="width:250px" ></elinput>
</el-form-item>
<el-form-item>
<el-button type="primary">审核</el-button>
<el-button>拒绝</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="账户信息" name="second">账户信息</el-tab-pane>
<el-tab-pane label="交易记录" name="third">交易记录</el-tab-pane>
</el-tabs>
</el-card>
</div>
</div>
</template>
<script>
import {detail} from '@/api/base/saasClient'
export default {
name: 'saas-clients-detail',
data () {
return {
activeName: 'first',
formData:{}
}
},
methods: {
detail(id) {
detail({id:id}).then(res => {
this.formData = res.data.data
console.log(id)
console.log(this.formData)
})
}
},
// 创建完毕状态
created() {
var id = this.$route.params.id
this.detail(id);
}
}</script>
<style rel="stylesheet/scss" lang="scss" scoped> .alert {
margin: 10px 0px 0px 0px; }.pagination {
margin-top: 10px;
text-align: right; }
</style>

4.4 接口对接

( 1 )启动第一天的企业微服务服务

( 2 )在config/dev.env.js中配置请求地址

1
2
3
4
5
6
7
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:9001/"'
})

第 3 章-SaaS系统用户权限设计

学习目标:

  • 理解RBAC模型的基本概念及设计思路
  • 了解SAAS-HRM中权限控制的需求及表结构分析
  • 完成组织机构的基本CRUD操作
  • 完成用户管理的基本CRUD操作
  • 完成角色管理的基本CRUD操作

1 组织机构管理

1.1 需求分析

1.1.1 需求分析

实现企业组织结构管理,实现部门的基本CRUD操作

image-20220922091303995

1.1.2 数据库表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE `co_department` (
`id` varchar( 40 ) NOT NULL,
`company_id` varchar( 255 ) NOT NULL COMMENT '企业ID',
`parent_id` varchar( 255 ) DEFAULT NULL COMMENT '父级部门ID',
`name` varchar( 255 ) NOT NULL COMMENT '部门名称',
`code` varchar( 255 ) NOT NULL COMMENT '部门编码',
`category` varchar( 255 ) DEFAULT NULL COMMENT '部门类别',
`manager_id` varchar( 255 ) DEFAULT NULL COMMENT '负责人ID',
`city` varchar( 255 ) DEFAULT NULL COMMENT '城市',
`introduce` text COMMENT '介绍',
`create_time` datetime NOT NULL COMMENT '创建时间',
`manager` varchar( 40 ) DEFAULT NULL COMMENT '部门负责人',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

1.2 微服务实现

1.2.1 抽取公共代码

( 1 ) 在公共controller

ihrm_commoncom.模块下的ihrm.common.controller包下添加公共controller

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
package com.ihrm.common.controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 公共controller
*     获取request,response
*     获取企业id,获取企业名称
*/
public class BaseController {
   protected HttpServletRequest request;
   protected HttpServletResponse response;
   @ModelAttribute
   public void setReqAndResp(HttpServletRequest request, HttpServletResponse response)
{
       this.request = request;
       this.response = response;
  }
   //企业id,(暂时使用1,以后会动态获取)
   public String parseCompanyId() {
       return "1";
  }
   public String parseCompanyName() {
       return "江苏传智播客教育股份有限公司";
  }
}

( 2 ) 公共service

ihrm_commoncom.模块下的ihrm.common.service包下添加公共BaseService

1
2
3
4
5
6
7
8
9
10
11
public class BaseService<T> {
protected Specification<T> getSpecification(String companyId) {
return new Specification<T>() {
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery,
CriteriaBuilder cb) {
return cb.equal(root.get("companyId").as(String.class),companyId);
}
};
}
}

1.2.2 实现基本CRUD操作

( 1 )实体类

在com.ihrm.domain.company包下创建Department实体类

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
package com.ihrm.domain.company;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* (Department)实体类
*/
@Entity
@Table(name = "co_department")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department implements Serializable {
private static final long serialVersionUID = -9084332495284489553L;
//ID
@Id
private String id;
/**
    * 父级ID
    */
private String pid;
/**
    * 企业ID
    */
private String companyId;
/**
    * 部门名称
    */
private String name;
/**
    * 部门编码,同级部门不可重复
    */
private String code;
/**
    * 负责人ID
    */
private String managerId;
/**
* 负责人名称
*/
private String manager;
/**
    * 介绍
    */
private String introduce;
/**
    * 创建时间
    */
private Date createTime;
}

( 2 )持久化层

在com.ihrm.company.dao包下创建DepartmentDao

1
2
3
4
5
6
7
8
9
package com.ihrm.company.dao;
import com.ihrm.domain.company.Department;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* 部门操作持久层
*/
public interface DepartmentDao extends JpaRepository<Department, String>, JpaSpecificationExecutor<Department> {
}

( 3 )业务层

在com.ihrm.company.service包下创建DepartmentService

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
package com.ihrm.company.service;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.common.exception.CommonException;
import com.ihrm.common.service.BaseService;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.company.dao.DepartmentDao;
import com.ihrm.domain.company.Department;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.Date;
import java.util.List;
/**
* 部门操作业务逻辑层
*/
@Service
public class DepartmentService extends BaseService {
@Autowired
private IdWorker idWorker;
@Autowired
private DepartmentDao departmentDao;
/**
    * 添加部门
    */
public void save(Department department) {
//填充其他参数
department.setId(idWorker.nextId() + "");
department.setCreateTime(new Date());
departmentDao.save(department);
}
/**
    * 更新部门信息
    */
public void update(Department department) {
Department sourceDepartment = departmentDao.findById(department.getId()).get();
sourceDepartment.setName(department.getName());
sourceDepartment.setPid(department.getPid());
sourceDepartment.setManagerId(department.getManagerId());
sourceDepartment.setIntroduce(department.getIntroduce());
sourceDepartment.setManager(department.getManager());
departmentDao.save(sourceDepartment);
}
/**
    * 根据ID获取部门信息
    *
    * @param id 部门ID
    * @return 部门信息
    */
public Department findById(String id) {
return departmentDao.findById(id).get();
}
/**
    * 删除部门
    *
    * @param id 部门ID
    */
public void delete(String id) {
departmentDao.deleteById(id);
}
/**
    * 获取部门列表
    */
public List<Department> findAll(String companyId) {
return departmentDao.findAll(getSpecification(companyId));
}
}

( 4 )控制层

在ihrm.company.controller创建控制器类DepartmentController

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
package com.ihrm.company.controller;
import com.ihrm.common.controller.BaseController;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.company.service.CompanyService;
import com.ihrm.company.service.DepartmentService;
import com.ihrm.domain.company.Company;
import com.ihrm.domain.company.Department;
import com.ihrm.domain.company.response.DeptListResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
/**
* 控制器层
*/
@RestController
@RequestMapping("/company")
public class DepartmentController extends BaseController{
@Autowired
private DepartmentService departmentService;
@Autowired
private CompanyService companyService;
/**
    * 添加部门
    */
@RequestMapping(value = "/departments", method = RequestMethod.POST)
public Result add(@RequestBody Department department) throws Exception {
department.setCompanyId(parseCompanyId());
departmentService.save(department);
return Result.SUCCESS();
}
/**
    * 修改部门信息
    */
@RequestMapping(value = "/departments/{id}", method = RequestMethod.PUT)
public Result update(@PathVariable(name = "id") String id, @RequestBody Department
department) throws Exception {
department.setCompanyId(parseCompanyId());
department.setId(id);
departmentService.update(department);
return Result.SUCCESS();
}
/**
    * 删除部门
    */
@RequestMapping(value = "/departments/{id}", method = RequestMethod.DELETE)
public Result delete(@PathVariable(name = "id") String id) throws Exception {
departmentService.delete(id);
return Result.SUCCESS();
}
/**
    * 根据id查询
    */
@RequestMapping(value = "/departments/{id}", method = RequestMethod.GET)
public Result findById(@PathVariable(name = "id") String id) throws Exception {
Department department = departmentService.findById(id);
return new Result(ResultCode.SUCCESS,department);
}
/**
    * 组织架构列表
    */
@RequestMapping(value = "/departments", method = RequestMethod.GET)
public Result findAll() throws Exception {
Company company = companyService.findById(parseCompanyId());
List<Department> list = departmentService.findAll(parseCompanyId());
return  new Result(ResultCode.SUCCESS,new DeptListResult(company,list));
}
}

1.3 前端实现

1.3.1 创建模块

( 1 )使用命令行创建module-departments模块并引入到工程中

1
itheima moduleAdd departments

( 2 )在src/main.js中注册模块

1
2
import departments from '@/module-departments/' // 组织机构管理
Vue.use(departments, store)

( 3 )在/module-departments/router/index.js配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Layout from '@/module-dashboard/pages/layout'
const _import = require('@/router/import_' + process.env.NODE_ENV)
export default [
{
root: true,
path: '/departments',
component: Layout,
redirect: 'noredirect',
name: 'departments',
meta: {
title: '组织架构管理',
icon: 'architecture'
},
children: [
{
path: 'index',
component: _import('departments/pages/index'),
name: 'organizations-index',
meta: {title: '组织架构', icon: 'architecture', noCache: true}
}
]
}
]

1.3.2 配置请求API

在/src/api/base/创建departments.js作为组织机构管理的API公共接口方法

1
2
3
4
5
6
7
8
9
10
import {
createAPI, createFileAPI
} from '@/utils/request'
export const organList = data => createAPI('/company/departments', 'get', data)
export const add = data => createAPI('/company/departments', 'post', data)
export const update = data => createAPI(`/company/departments/${data.id}`, 'put', data)
export const detail = data => createAPI(`/company/departments/${data.id}`, 'get', data)
export const remove = data => createAPI(`/company/departments/${data.id}`, 'delete',data)
export const changeDept = data => createAPI(`/company/departments/changeDept`, 'put', data)
export const saveOrUpdate = data => {return data.id?update(data):add(data)}

1.3.3 构造列表

( 1 )构造基本页面样式

找到/module-departments/page/index.vue,使用element-ui提供的Card组件构造卡片式容器

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
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card shadow="never">
<div class='organization-index'>
<div class='organization-index-top'>
<div class='main-top-title'>
<el-tabs v-model="activeName">
<el-tab-pane label="组织结构" name="first"></el-tab-pane>
<div class="el-tabs-report">
<a class="el-button el-button--primary el-button--mini" title="导 出" >导入</a>
<a class="el-button el-button--primary el-button--mini" title="导 出" >导出</a>
</div>
</el-tabs>
</div>
</div>
<div style="overflow: scroll;white-space:nowrap"  class="treBox">
<div class="treeCon clearfix">
<span>
<i class="fa fa-university" aria-hidden="true"></i>
<span ><strong>{{departData.name}}</strong></span>
</span>
<div class="fr">
<div class="treeRinfo">
<span>负责人</span>
<span>在职  <em class="colGreen" title="在职人数">---
</em>&nbsp;&nbsp;(<em class="colGreen" title="正式员工">---</em>&nbsp;/&nbsp;<em
class="colRed" title="非正式员工">---</em>)</span>
</div>
<div class="treeRinfo">
<el-dropdown class="item">
<span class="el-dropdown-link">
操作<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>
<el-button type="text" @click="handlAdd('')">添加子部门
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button type="text"
@click="handleList(organizationTree,0)">查看待分配员工</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>  
</div>
</div>
<!--
构造树形列表
-->
</div>
</div>    
</el-card>
</div>
</div>
</template>
<style rel="stylesheet/scss" lang="scss">
.el-dropdown {
color: #000000
}
.el-tree-node__content>.el-tree-node__expand-icon {
padding:0px; }
.el-tree-node__expand-icon {
color:#ffffff
}
.generalClassNode {
padding-left: 20px; }
.el-tree-node__content{
font-size: 16px;
line-height: 36px;
height:36px; }
.custom-tree-node{
padding-left: 20px; }
.objectTree {
overflow: auto;
z-index: 100;
width: 300px;
border: 1px solid #dcdfe6;
margin-top: 5px;
left: 70px; }
.el-tabs__content {
overflow: initial; }
.boxpad {
margin-left: -40px; }
.boxpad > div:first-child,
.objectTree > div:first-child.el-tree-node > div:first-child {
display: none; }
</style>
<style  rel="stylesheet/scss" lang="scss" scoped>
.el-tree-node__expand-icon{ }
.el-icon-caret-right{}
.el-tree-node__content{
font-size: 14px;
line-height: 36px; }
.generalClass {
font-size: 14px;
line-height: 36px;
color:#000000
}
.all {
position: relative;
min-height: 100%;
padding-bottom: 200px; }
.organization-main:after,
.organization-index-top:after {
display: block;
clear: both;
content: '';
visibility: hidden;
height: 0; }
.organization-main {
font-size: 14px;
font-size: 14px; }
.organization-index {
padding-bottom: 20px;
margin-left: 20px; }
.main-top-title {
padding-left: 20px;
padding-top: 20px;
text-align: left; }
::-webkit-scrollbar-thumb {
background-color: #018ee8;
height: 50px;
outline-offset: -2px;
outline: 8px solid #fff;
-webkit-border-radius: 4px; }
::-webkit-scrollbar-track-piece {
background-color: #fff;
-webkit-border-radius: 0; }
::-webkit-scrollbar {
width: 8px;
height: 8px; }
::-webkit-scrollbar-thumb:hover {
background-color: #fb4446;
height: 50px;
-webkit-border-radius: 4px; }
.modal-total {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
background: #000;
z-index: 90;
opacity: 0.2; }
.modal {
width: 400px;
height: 300px;
background-color: #ffffff;
z-index: 999;
position: absolute;
left: 45%;
top: 20%;
text-align: center; }
.treBox {
padding: 30px 120px 0;
}
.organization-index-top {
position: relative;
.el-tabs-report {
position: absolute;
top: -50px;
right: 15px;
}
}
.treeCon {
border-bottom: 1px solid #cfcfcf;
padding: 10px 0;
margin-bottom: 10px;
.el-dropdown {
color: #333;
}
}
.treeRinfo {
display: inline-block; }
.treeRinfo span {
padding-left: 30px; }
</style>

( 2 )树形机构列表

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
<el-tree :data="departData.children" :indent="20">
<div class="generalClass" slot-scope="{node,data}" style="width:99%">
<span>
<span>
<span>
<i v-if="node.isLeaf" class="fa fa-male"></i>
<i v-else :class="node.expanded ? 'fa fa-minus-square-o':
'fa fa-plus-square-o'"></i>
<span><strong>{{ data.name }}</strong></span>
</span>                      
</span>                    
</span>
<div class=fr>
<span class="treeRinfo">
<div class="treeRinfo">
<span>负责人</span>
<span>在职  <em class="colGreen" title="在职人数">---
</em>&nbsp;&nbsp;(<em class="colGreen" title="正式员工">---</em>&nbsp;/&nbsp;<em
class="colRed" title="非正式员工">---</em>)</span>  
</div>
<el-dropdown class="item">
<span class="el-dropdown-link">
操作<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>
<el-button type="text" @click="handlAdd(data.id)">添加子
部门</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button type="text" @click="handleEdit(data.id)">编辑
部门</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button type="text" @click="handleList(treeRoot,1)">查
看员工</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button type="text" @click="handleDelete(data)">删除
</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>                  
</div>
</div>
</el-tree>

( 3 ) 构造数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//数据绑定模型
data() {
return {
activeName: 'first',  //激活pane的名称
dialogFormVisible:false,//是否显示弹出层标识
parentId:'', //父id
departData:{}, //部门列表
formData:{} //表单提交数据
}
},
//自定义方法
methods: {
getObject(params) {
organList().then(res => {
this.departData = res.data.data
})
}
},
//钩子函数
created: function() {
this.getObject()
},

1.3.4 组织机构的增删改查

( 1 )新增部门

使用element-ui提供的dialog的弹出层构造弹出添加页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<el-dialog title="编辑部门" :visible.sync="dialogFormVisible">
<el-form ref="dataForm" :model="formData" label-width="120px">
<el-form-item label="部门名称">
<el-input v-model="formData.name" placeholder='请输入部门名称'></el-input>
</el-form-item>
<el-form-item label="部门编码">
<el-input v-model="formData.code" placeholder='请输入部门编码'></el-input>
</el-form-item>
<el-form-item label="部门负责人">
<el-input v-model="formData.manager" placeholder='请输入负责人'></el-input>
</el-form-item>
<el-form-item label="部门介绍">
<el-input v-model="formData.introduce" placeholder='请输入介绍'></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="createData">确定</el-button>
<el-button @click="dialogFormVisible=false">取消</el-button>
</div>
</el-dialog>

配置保存方法

1
2
3
4
5
6
7
8
9
createData:function() {
this.formData.parentId = this.parentId
saveOrUpdate(this.formData)
.then(res => {
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
location.reload()
this.dialogFormVisible=false
})
}

( 2 )修改部门

  1. 根据id查询部门
1
2
3
4
5
6
7
handleEdit(id) {
detail({id}).then( res=> {
this.formData = res.data.data
this.dialogFormVisible = true
this.parentId = res.data.data.parentId
})
}
  1. 调用方法更新部门

( 3 )删除部门

1
2
3
4
5
6
7
8
9
10
11
handleDelete(obj) {
this.$confirm(
`本次操作将删除${obj.name},删除后将不可恢复,您确认删除吗?`
).then(() => {
remove({id:obj.id}).then( res=> {

this.$message({message:res.data.message,type:res.data.success?"success":"error"});
location.reload()
})
})
},

1.3.5 抽取组件

组件(Component)是Vue.js 最强大的功能。可以通过将不同的业务拆分为不同的组件进行开发,让代码更加优

雅提供可读性。当然页可以封装可重用的代码,通过传入对象的不同,实现组件的复用。

( 1 )抽取新增/修改页面到/module-departments/components/add.vue中

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
<template>
<el-dialog title="编辑部门" :visible.sync="dialogFormVisible">
<el-form ref="dataForm" :model="formData" label-width="120px">
<el-form-item label="部门名称">
<el-input v-model="formData.name" placeholder='请输入部门名称'></el-input>
</el-form-item>
<el-form-item label="部门编码">
<el-input v-model="formData.code" placeholder='请输入部门编码'></el-input>
</el-form-item>
<el-form-item label="部门负责人">
<el-input v-model="formData.manager" placeholder='请输入部门负责人'></el-input>
</el-form-item>
<el-form-item label="部门介绍">
<el-input v-model="formData.introduce" placeholder='请输入部门介绍'></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="createData">确定</el-button>
<el-button @click="dialogFormVisible=false">取消</el-button>
</div>
</el-dialog>
</template>
<script>
import { saveOrUpdate } from '@/api/base/departments'
export default {
name: 'dept-add',
data() {
return {
dialogFormVisible:false,
formData:{},
parentId:''
}
},
methods: {
createData:function() {
this.formData.parentId = this.parentId
saveOrUpdate(this.formData)
.then(res => {

this.$message({message:res.data.message,type:res.data.success?"success":"error"});
location.reload()
this.dialogFormVisible=false
})
}
}
}</script>

( 2 ) 在/module-departments/page/index.vue中引用组件

导入组件

1
2
3
4
5
6
7
8
9
10
11
12
import deptAdd from './../components/add'  //导入组件
export default {
//声明引用组件
components: { deptAdd }, //声明组件
data() {
return {
deptAdd: 'deptAdd', //配置组件别名
activeName: 'first',
departData:{},
}
},
.... }

使用组件

1
2
3
//v-bind:is (绑定的组件名称)
//ref : 引用子组件中内容的别名
<component v-bind:is="deptAdd" ref="deptAdd"></component>

改造新增修改方法

1
2
3
4
5
6
7
8
9
10
11
12
13
handlAdd(parentId) {
//对子组件中的属性复制
this.$refs.deptAdd.formData = {};
this.$refs.deptAdd.parentId = parentId
this.$refs.deptAdd.dialogFormVisible = true;
},
handleEdit(id) {
detail({id}).then( res=> {
this.$refs.deptAdd.formData = res.data.data
this.$refs.deptAdd.dialogFormVisible = true
this.$refs.deptAdd.parentId = res.data.data.parentId
})
},

2 RBAC模型

2.1 什么是RBAC

RBAC(全称:Role-Based Access Control)基于角色的权限访问控制,作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系可以建立起来以囊括更广泛的客观情况。

访问控制是针对越权使用资源的防御措施,目的是为了限制访问主体(如用户等) 对访问客体(如数据库资源等)的访问权限。企业环境中的访问控制策略大部分都采用基于角色的访问控制(RBAC)模型,是目前公认的解决大型企业的统一资源访问控制的有效方法

2.2 基于RBAC的设计思路

基于角色的访问控制基本原理是在用户和访问权限之间加入角色这一层,实现用户和权限的分离,用户只有通过激活角色才能获得访问权限。通过角色对权限分组,大大简化了用户权限分配表,间接地实现了对用户的分组,提高了权限的分配效率。且加入角色层后,访问控制机制更接近真实世界中的职业分配,便于权限管理。

image-20220922091626176

在RBAC模型中,角色是系统根据管理中相对稳定的职权和责任来划分,每种角色可以完成一定的职能。用户通过饰演不同的角色获得角色所拥有的权限,一旦某个用户成为某角色的成员,则此用户可以完成该角色所具有的职能。通过将权限指定给角色而不是用户,在权限分派上提供了极大的灵活性和极细的权限指定粒度。

3.3 表结构分析

image-20220922091638574

一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。

3 SAAS-HRM中的权限设计

3.1 需求分析

3.1.1 SAAS平台的基本元素

image-20220922091653918

SAAS平台管理员:负责平台的日常维护和管理,包括用户日志的管理、租户账号审核、租户状态管理、租户费用的管理,要注意的是平台管理员不能对租户的具体业务进行管理

  • 企业租户:指访问SaaS平台的用户企业,在SaaS平台中各租户之间信息是独立的。
  • 租户管理员:为租户角色分配权限和相关系统管理、维护。
  • 租户角色:根据业务功能租户管理员进行角色划分,划分好角色后,租户管理员可以对相应的角色进行权限分配
  • 租户用户:需对租户用户进行角色分配,租户用户只能访问授权的模块信息。

3.1.2 需求分析

在应用系统中,权限是以什么样的形式展现出来的?对菜单的访问,页面上按钮的可见性,后端接口的控制,都要进行充分考虑

前端

  • 前端菜单:根据是否有请求菜单权限进行动态加载
  • 按钮:根据是否具有此权限点进行显示/隐藏的控制

后端

  • 前端发送请求到后端接口,有必要对接口的访问进行权限的验证

3.2 权限设计

针对这样的需求,在有些设计中可以将菜单,按钮,后端API请求等作为资源,这样就构成了基于RBAC的另一种授权模型(用户-角色-权限-资源)。在SAAS-HRM系统的权限设计中我们就是才用了此方案image-20220922091723207

针对此种权限模型,其中权限究竟是属于菜单,按钮,还是API的权限呢?那就需要在设计数据库权限表的时候添加类型加以区分(如权限类型 1 为菜单 2 为功能 3 为API)。

3.3 表结构分析

image-20220922091751328

这里要注意的是,权限表与权限菜单表、页面元素表与API接口表都是一对一的关系与传统的RBAC模型对比不难发现此种设计的好处:

  1. 不需要区分哪些是操作,哪些是资源

  2. 方便扩展,当系统要对新的东西进行权限控制时,我只需要建立一个新的资源表,并确定这类权限的权限类型标识即可。

4 用户管理

4.1 需求分析

用户其实就是saas企业访问的员工,对企业员工完成基本的CRUD操作

表结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE `bs_user` (
`id` varchar( 40 ) NOT NULL COMMENT 'ID',
`mobile` varchar( 40 ) NOT NULL COMMENT '手机号码',
`username` varchar( 255 ) NOT NULL COMMENT '用户名称',
`password` varchar( 255 ) DEFAULT NULL COMMENT '密码',
`enable_state` int( 2 ) DEFAULT '1' COMMENT '启用状态 0 是禁用, 1 是启用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`department_id` varchar( 40 ) DEFAULT NULL COMMENT '部门ID',
`time_of_entry` datetime DEFAULT NULL COMMENT '入职时间',
`form_of_employment` int( 1 ) DEFAULT NULL COMMENT '聘用形式',
`work_number` varchar( 20 ) DEFAULT NULL COMMENT '工号',
`form_of_management` varchar( 8 ) DEFAULT NULL COMMENT '管理形式',
`working_city` varchar( 16 ) DEFAULT NULL COMMENT '工作城市',
`correction_time` datetime DEFAULT NULL COMMENT '转正时间',
`in_service_status` int( 1 ) DEFAULT NULL COMMENT '在职状态 1.在职 2.离职',
`company_id` varchar( 40 ) DEFAULT NULL COMMENT '企业ID',
`company_name` varchar( 40 ) DEFAULT NULL,
`department_name` varchar( 40 ) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_phone` (`mobile`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

4.2 配置系统微服务

( 1 )搭建系统微服务模块(ihrm_system),pom引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.ihrm</groupId>
<artifactId>ihrm_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

( 2 )配置application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9002
spring:
application:
name: ihrm-system #指定服务名
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/ihrm?useUnicode=true&characterEncoding=utf8
username: root
password: 111111
jpa:
database: MySQL
show-sql: true
open-in-view: true

( 3 )配置启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.ihrm.system;
import com.ihrm.common.utils.IdWorker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
@SpringBootApplication(scanBasePackages = "com.ihrm")
@EntityScan("com.ihrm.domain.system")
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class, args);
}
@Bean
public IdWorker idWorkker() {
return new IdWorker(1, 1);
}
}

4.3 后端用户基本操作

( 1 )实体类

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
package com.ihrm.domain.system;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
* 用户实体类
*/
@Entity
@Table(name = "bs_user")
@Getter
@Setter
public class User implements Serializable {
private static final long serialVersionUID = 4297464181093070302L;
/**
    * ID
    */
@Id
private String id;
/**
    * 手机号码
    */
private String mobile;
/**
    * 用户名称
    */
private String username;
/**
    * 密码
    */
private String password;
/**
    * 启用状态 0为禁用 1为启用
    */
private Integer enableState;
/**
    * 创建时间
    */
private Date createTime;
private String companyId;
private String companyName;
/**
    * 部门ID
    */
private String departmentId;
/**
    * 入职时间
    */
private Date timeOfEntry;
/**
    * 聘用形式
    */
private Integer formOfEmployment;
/**
    * 工号
    */
private String workNumber;
/**
    * 管理形式
    */
private String formOfManagement;
/**
    * 工作城市
    */
private String workingCity;
/**
    * 转正时间
    */
private Date correctionTime;
/**
    * 在职状态 1.在职 2.离职
    */
private Integer inServiceStatus;
private String departmentName;
@ManyToMany
@JsonIgnore
@JoinTable(name="pe_user_role",joinColumns= {@JoinColumn(name="user_id",referencedColumnName="id")},
inverseJoinColumns={@JoinColumn(name="role_id",referencedColumnName="id")}
)
private Set<Role> roles = new HashSet<Role>();//用户与角色   多对多
}

( 2 )持久化层

1
2
3
4
5
6
7
8
9
10
package com.ihrm.system.dao;
import com.ihrm.system.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
 * 企业数据访问接口
 */
public interface UserDao extends JpaRepository<User, String>,
JpaSpecificationExecutor<User> {
}

( 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
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
package com.ihrm.system.service;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.domain.system.Role;
import com.ihrm.domain.system.User;
import com.ihrm.system.dao.RoleDao;
import com.ihrm.system.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.*;
/**
* 部门操作业务逻辑层
*/
@Service
public class UserService {
@Autowired
private IdWorker idWorker;
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
public User findByMobileAndPassword(String mobile, String password) {
User user = userDao.findByMobile(mobile);
if (user != null && password.equals(user.getPassword())) {
return user;
} else {
return null;
}
}
/**
    * 添加用户
    */
public void save(User user) {
//填充其他参数
user.setId(idWorker.nextId() + "");
user.setCreateTime(new Date()); //创建时间
user.setPassword("123456");//设置默认登录密码
user.setEnableState(1);//状态
userDao.save(user);
}
/**
    * 更新用户
    */
public void update(User user) {
User targer = userDao.getOne(user.getId());
targer.setPassword(user.getPassword());
targer.setUsername(user.getUsername());
targer.setMobile(user.getMobile());
targer.setDepartmentId(user.getDepartmentId());
targer.setDepartmentName(user.getDepartmentName());
userDao.save(targer);
}
/**
    * 根据ID查询用户
    */
public User findById(String id) {
return userDao.findById(id).get();
}
/**
    * 删除部门
    *
    * @param id 部门ID
    */
public void delete(String id) {
userDao.deleteById(id);
}
public Page<User> findSearch(Map<String,Object> map, int page, int size) {
return userDao.findAll(createSpecification(map), PageRequest.of(page-1, size));
}
/**
    * 调整部门
    */
public void changeDept(String deptId,String deptName,List<String> ids) {
for (String id : ids) {
User user = userDao.findById(id).get();
user.setDepartmentName(deptName);
user.setDepartmentId(deptId);
userDao.save(user);
}
}
/**
    * 分配角色
    */
public void assignRoles(String userId,List<String> roleIds) {
User user = userDao.findById(userId).get();
Set<Role> roles = new HashSet<>();
for (String id : roleIds) {
Role role = roleDao.findById(id).get();
roles.add(role);
}
//设置用户和角色之间的关系
user.setRoles(roles);
userDao.save(user);
}
/**
    * 动态条件构建
    * @param searchMap
    * @return
    */
private Specification<User> createSpecification(Map searchMap) {
return new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
List<Predicate> predicateList = new ArrayList<Predicate>();
// ID
if (searchMap.get("id") !=null && !"".equals(searchMap.get("id"))) {
predicateList.add(cb.equal(root.get("id").as(String.class),
(String)searchMap.get("id")));
}
// 手机号码
if (searchMap.get("mobile")!=null &&
!"".equals(searchMap.get("mobile"))) {
predicateList.add(cb.equal(root.get("mobile").as(String.class),
(String)searchMap.get("mobile")));
}
// 用户ID
if (searchMap.get("departmentId")!=null &&
!"".equals(searchMap.get("departmentId"))) {
predicateList.add(cb.like(root.get("departmentId").as(String.class),
(String)searchMap.get("departmentId")));
}
// 标题
if (searchMap.get("formOfEmployment")!=null &&
!"".equals(searchMap.get("formOfEmployment"))) {

predicateList.add(cb.like(root.get("formOfEmployment").as(String.class),
(String)searchMap.get("formOfEmployment")));
}
if (searchMap.get("companyId")!=null &&
!"".equals(searchMap.get("companyId"))) {
predicateList.add(cb.like(root.get("companyId").as(String.class),
(String)searchMap.get("companyId")));
}
if (searchMap.get("hasDept")!=null &&
!"".equals(searchMap.get("hasDept"))) {
if("0".equals((String)searchMap.get("hasDept"))) {
predicateList.add(cb.isNull(root.get("departmentId")));
}else{
predicateList.add(cb.isNotNull(root.get("departmentId")));
}
}
return cb.and( predicateList.toArray(new
Predicate[predicateList.size()]));
}
};
}
}

( 4 )控制器层

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
package com.ihrm.system.controller;
import com.ihrm.common.controller.BaseController;
import com.ihrm.common.entity.PageResult;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.domain.system.User;
import com.ihrm.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/sys")
public class UserController extends BaseController {
@Autowired
private UserService userService;
//保存用户
@RequestMapping(value = "/user", method = RequestMethod.POST)
public Result add(@RequestBody User user) throws Exception {
user.setCompanyId(parseCompanyId());
user.setCompanyName(parseCompanyName());
userService.save(user);
return Result.SUCCESS();
}
//更新用户
@RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)
public Result update(@PathVariable(name = "id") String id, @RequestBody User user)
throws Exception {
userService.update(user);
return Result.SUCCESS();
}
//删除用户
@RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
public Result delete(@PathVariable(name = "id") String id) throws Exception {
userService.delete(id);
return Result.SUCCESS();
}
/**
    * 根据ID查询用户
    */
@RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public Result findById(@PathVariable(name = "id") String id) throws Exception {
User user = userService.findById(id);
return new Result(ResultCode.SUCCESS,user);
}
/**
    * 分页查询用户
    */
@RequestMapping(value = "/user", method = RequestMethod.GET)
public Result findByPage(int page,int pagesize,@RequestParam Map<String,Object>
map) throws Exception {
map.put("companyId",parseCompanyId());
Page<User> searchPage = userService.findSearch(map, page, pagesize);
PageResult<User> pr = new
PageResult(searchPage.getTotalElements(),searchPage.getContent());
return new Result(ResultCode.SUCCESS,pr);
}
}

4.4 前端用户基本操作

由于课程时间有限,本着不浪费时间的原则,页面部分的基本功能都是大致相似的。课上我们使用提供的基本模块代码构建模块信息。

4.4.1 配置接口请求路径

在config/index.js中通过proxyTable配置代理转发的请求后端地址

1
2
3
4
5
6
7
'/api/sys': {
target: 'http://localhost:9002/sys',
changeOrigin: true,
pathRewrite: {
'^/api/sys': ''
}
},

4.4.2 导入员工模块

注册模块

1
2
import employees from '@/module-employees/' // 员工管理
Vue.use(employees, store)

在/src/api/base/下配置API(user.js)

1
2
3
4
5
6
7
import {createAPI} from '@/utils/request'
export const list = data => createAPI('/sys/user', 'get', data)
export const simple = data => createAPI('/sys/user/simple', 'get', data)
export const add = data => createAPI('/sys/user', 'post', data)
export const update = data => createAPI(`/sys/user/${data.id}`, 'put', data)
export const remove = data => createAPI(`/sys/user/${data.id}`, 'delete', data)
export const detail = data => createAPI(`/sys/user/${data.id}`, 'get', data)

4.4.3 用户列表展示

( 1 ) 页面代码

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
<el-table :data="dataList" fit style="width: 100%;" border>
<el-table-column type="index" :index="1" label="序号" width="150"> </el-tablecolumn>
<el-table-column sortable prop="username" label="姓名" width="150"></el-tablecolumn>
<el-table-column sortable prop="mobile" label="手机号" width="150"></el-tablecolumn>
<el-table-column sortable prop="workNumber" label="工号" width="120"></eltable-column>
<el-table-column sortable prop="formOfEmployment" label="聘用形势"
width="200"></el-table-column>
<el-table-column sortable prop="departmentName" label="部门" width="200"></eltable-column>
<el-table-column sortable prop="timeOfEntry" label="入职时间" width="150">
</el-table-column>
<el-table-column sortable label="状态" width="120">
<template slot-scope="scope">
<el-switch
v-model="scope.row.accountStatus"
active-color="#13ce66"
inactive-color="#ff4949"
@change="handleStatus(scope.row)">
</el-switch>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" align="center" width="220">
<template slot-scope="scope">
<router-link :to="{'path':'/employees/details/' + scope.row.id}"
class="el-button el-button--text el-button--small">
查看
</router-link>
<el-button @click="handleDelete(scope.row)" type="text" size="small">删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<PageTool :paginationPage="requestParameters.page"
:paginationPagesize="requestParameters.pagesize" :total="counts"
@pageChange="handleCurrentChange" @pageSizeChange="handleSizeChange">
</PageTool>
</div>

(2)js构造数据

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
import constantApi from '@/api/constant/employees'
import {list,remove} from "@/api/base/users"
import PageTool from './../../components/page/page-tool'
import employeesAdd from './../components/add'
var _this = null
export default {
name: 'employeesList',
components: {
PageTool,employeesAdd
},
data() {
return {
employeesAdd: 'employeesAdd',
baseData: constantApi,
dataList: [],
counts: '',
requestParameters:{
page: 1,
pagesize: 10,
}    
}
},
methods: {
// 业务方法
doQuery(params) {
list(this.requestParameters).then(res => {
this.dataList = res.data.data.rows
this.counts = res.data.data.total
})
}
},
// 创建完毕状态
created: function() {
this.doQuery()
},
}

4.4.4 用户详情

( 1 ) 配置路由

1
2
3
4
5
6
7
8
9
{
       path: 'details/:id',
       component: _import('employees/pages/employees-details'),
       // hidden: true // 是否显示在左侧菜单
       name: 'details',
       meta: {
         title: '详情'
      }
    }

( 2 ) 完成用户详情页面

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
<template>
<div class="dashboard-container">
<div class="app-container">
<el-card :style="{minHeight:boxHeight}">
<el-tabs v-model="activeName" class="infoPosin">
<el-tab-pane name="first" class="rInfo">
<span slot="label">登录账户设置</span>
<component v-bind:is="accountInfo" :objId='objId' ref="user"></component>
</el-tab-pane>
<el-tab-pane name="two" class="rInfo">
<span slot="label">个人详情</span>
</el-tab-pane>
<el-tab-pane name="third" class="rInfo">
<span slot="label">岗位信息</span>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</div>
</template>
<script>
import accountInfo from './../components/details-account-info'
export default {
name: 'employeesDetails',
components: { accountInfo},
data() {
return {
accountInfo:'accountInfo',
activeName: 'first',
objId: this.$route.params.id,
dataList: []
}
}
}</script>

( 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
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
<template>
<div class="boxInfo">
<!-- 表单内容 -->
<div class="formInfo">
<div>
<!-- 头部信息  -->
<div class="userInfo">
<div class="headInfo clearfix">
<div class="headText">
<el-form ref="formData" :model="formData" label-width="215px">
<el-form-item label="姓名:">
<el-input v-model="formData.username" placeholder="请输入"
class="inputW"></el-input>
</el-form-item>
<el-form-item label="密码:">
<el-input v-model="formData.password" placeholder="请输入"
class="inputW"></el-input>
</el-form-item>
<el-form-item label="部门:">
<el-input
placeholder="请选择"
v-model="formData.departmentName"
icon="caret-bottom"
class="inputW"
@click.native="isShowSelect = !isShowSelect">
</el-input>
<input v-model="formData.departmentId" type="hidden" >
<el-tree v-if="isShowSelect"
:expand-on-click-node="false"
:data="inspectionObjectOptions"
:props="{label:'name'}"
default-expand-all
:filter-node-method="filterNode"
@node-click="handleNodeClick"
class="objectTree"
ref="tree2">
</el-tree>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveData">更新</el-button>
<router-link :to="{'path':'/employees/index'}" class="el-button
el-button--text el-button--small">
取消
</router-link>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import constantApi from '@/api/constant/employees'
import {detail,update} from "@/api/base/users"
import { organList } from '@/api/base/departments'
export default {
name: 'accountInfo',
props: ['objId'],
data() {
return {
baseData: constantApi,
inspectionObjectOptions: [],
isShowSelect:false,
formData: {
id: this.objId,
}
}
},
methods: {
handleNodeClick(data) {
this.formData.departmentName = data.name
this.formData.departmentId = data.id
this.isShowSelect = false
},
// 获取详情
getObjInfo() {
detail({ id: this.objId }).then(res => {
this.formData = res.data.data
})
},
saveData(obj) {
update(this.formData)
.then(res => {
this.formData = res.data
this.$message.success('保存成功!')
this.getObjInfo()
})
},
},
// 创建完毕状态
created: function() {
this.getObjInfo()
organList().then(ret => {
this.inspectionObjectOptions.push(ret.data.data)
})
}
}</script>

4.4.5 用户的新增

和组织机构的增删改查大同小异,学员可以参照代码自行实现

5 作业-角色管理

5.1 需求分析

完成角色的基本CRUD操作

5.2 后端实现

( 1 )实体类

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.ihrm.domain.system;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "pe_role")
@Getter
@Setter
public class Role implements Serializable {
private static final long serialVersionUID = 594829320797158219L;
@Id
private String id;
/**
    * 角色名
    */
private String name;
/**
    * 说明
    */
private String description;
/**
    * 企业id
    */
private String companyId;
@JsonIgnore
@ManyToMany(mappedBy="roles")
private Set<User> users = new HashSet<User>(0);//角色与用户   多对多
@JsonIgnore
@ManyToMany
@JoinTable(name="pe_role_permission",
joinColumns={@JoinColumn(name="role_id",referencedColumnName="id")},
inverseJoinColumns= {@JoinColumn(name="permission_id",referencedColumnName="id")})
private Set<Permission> permissions = new HashSet<Permission>(0);//角色与模块 多对多
}

( 2 )持久化层

1
2
3
4
5
6
7
8
9
10
package com.ihrm.system.dao;
import com.ihrm.system.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
 * 企业数据访问接口
 */
public interface RoleDao extends JpaRepository<Role, String>,
JpaSpecificationExecutor<Role> {
}

( 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
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
package com.ihrm.system.service;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.system.dao.CompanyDao;
import com.ihrm.system.dao.RoleDao;
import com.ihrm.system.dao.UserDao;
import com.ihrm.system.domain.Company;
import com.ihrm.system.domain.Role;
import com.ihrm.system.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 角色操作业务逻辑层
*/
@Service
public class RoleService {
@Autowired
private IdWorker idWorker;
@Autowired
private RoleDao roleDao;
/**
    * 添加角色
    */
public void save(Role role) {
//填充其他参数
role.setId(idWorker.nextId() + "");
roleDao.save(role);
}
/**
    * 更新角色
    */
public void update(Role role) {
Role targer = roleDao.getOne(role.getId());
targer.setDescription(role.getDescription());
targer.setName(role.getName());
roleDao.save(targer);
}
/**
    * 根据ID查询角色
    */
public Role findById(String id) {
return roleDao.findById(id).get();
}
/**
    * 删除角色
    */
public void delete(String id) {
roleDao.deleteById(id);
}
public Page<Role> findSearch(String companyId, int page, int size) {
Specification<Role> specification = new Specification<Role>() {
@Override
public Predicate toPredicate(Root<Role> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
return cb.equal(root.get("companyId").as(String.class),companyId);
}
};
return roleDao.findAll(specification, PageRequest.of(page-1, size));
}
}

( 4 )控制器层

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
package com.ihrm.system.controller;
import com.ihrm.common.entity.PageResult;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.system.domain.Role;
import com.ihrm.system.domain.User;
import com.ihrm.system.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.bind.annotation.*;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/sys")
public class RoleController {

@Autowired
private RoleService roleService;
//添加角色
@RequestMapping(value = "/role", method = RequestMethod.POST)
public Result add(@RequestBody Role role) throws Exception {
String companyId = "1";
role.setCompanyId(companyId);
roleService.save(role);
return Result.SUCCESS();
}
//更新角色
@RequestMapping(value = "/role/{id}", method = RequestMethod.PUT)
public Result update(@PathVariable(name = "id") String id, @RequestBody Role role)
throws Exception {
roleService.update(role);
return Result.SUCCESS();
}
//删除角色
@RequestMapping(value = "/role/{id}", method = RequestMethod.DELETE)
public Result delete(@PathVariable(name = "id") String id) throws Exception {
roleService.delete(id);
return Result.SUCCESS();
}
/**
    * 根据ID获取角色信息
    */
@RequestMapping(value = "/role/{id}", method = RequestMethod.GET)
public Result findById(@PathVariable(name = "id") String id) throws Exception {
Role role = roleService.findById(id);
return new Result(ResultCode.SUCCESS,role);
}
/**
    * 分页查询角色
    */
@RequestMapping(value = "/role", method = RequestMethod.GET)
public Result findByPage(int page,int pagesize,Role role) throws Exception {
String companyId = "1";
Page<Role> searchPage = roleService.findSearch(companyId, page, pagesize);
PageResult<Role> pr = new
PageResult(searchPage.getTotalElements(),searchPage.getContent());
return new Result(ResultCode.SUCCESS,pr);
}
}

5.3 前端实现

学员参考资料自行实现

第 4 章 权限管理与jwt鉴权

学习目标:

  • 理解权限管理的需求以及设计思路
  • 实现角色分配和权限分配
  • 理解常见的认证机制
  • 能够使用JWT完成微服务Token签发与验证

1 权限管理

1.1 需求分析

完成权限(菜单,按钮(权限点),API接口)的基本操作

image-20220922100354122

权限与菜单,菜单与按钮,菜单与API接口都是一对一关系。为了方便操作,在SAAS-HRM系统的表设计中,采用基于共享主键的形式实现一对一关系维护,并且数据库约束,一切的关系维护需要程序员在代码中实现。

1.2 后端实现

1.2.1 实体类

在系统微服务中创建权限,菜单,按钮(权限点),API对象的实体类

( 1 ) 权限实体类Permission

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
@Entity
@Table(name = "pe_permission")
@Getter
@Setter
@NoArgsConstructor
@DynamicInsert(true)
@DynamicUpdate(true)
public class Permission implements Serializable {
   private static final long serialVersionUID = -4990810027542971546L;
   /**
    * 主键
    */
   @Id
   private String id;
   /**
    * 权限名称
    */
   private String name;
   /**
    * 权限类型 1为菜单 2为功能 3为API
    */
   private Integer type;
   /**
    * 权限编码
    */
   private String code;
   /**
    * 权限描述
    */
   private String description;
   private String pid;
   //可见状态
   private String enVisible;
   public Permission(String name, Integer type, String code, String description) {
       this.name = name;
       this.type = type;
       this.code = code;
       this.description = description;
  }
}

( 2 )权限菜单实体类PermissionMenu

1
2
3
4
5
6
7
8
9
10
11
@Entity
@Table(name = "pe_permission_menu")
@Getter
@Setter
public class PermissionMenu implements Serializable {
   private static final long serialVersionUID = -1002411490113957485L;
   @Id
   private String id; //主键
   private String menuIcon; //展示图标
   private String menuOrder; //排序号
}

( 3 )权限菜单(权限点)实体类PermissionPoint

1
2
3
4
5
6
7
8
9
10
11
@Entity
@Table(name = "pe_permission_point")
@Getter
@Setter
public class PermissionPoint implements Serializable {
   private static final long serialVersionUID = -1002411490113957485L;
   @Id
   private String id;
   private String pointClass;
   private String pointIcon;
   private String pointStatus; }

( 4 )权限API实体类PermissionApi

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Table(name = "pe_permission_api")
@Getter
@Setter
public class PermissionApi implements Serializable {
   private static final long serialVersionUID = -1803315043290784820L;
   
   @Id
   private String id;
   private String apiUrl;
   private String apiMethod;
   private String apiLevel;//权限等级,1为通用接口权限,2为需校验接口权限
}

1.2.2 持久层

( 1 )权限持久化类

1
2
3
4
5
6
7
/**
 * 权限数据访问接口
 */
public interface PermissionDao extends JpaRepository<Permission, String>,
JpaSpecificationExecutor<Permission> {
   List<Permission> findByTypeAndPid(int type,String pid);
}

( 2 )权限菜单持久化类

1
2
3
public interface PermissionMenuDao extends JpaRepository<PermissionMenu, String>, 
JpaSpecificationExecutor<PermissionMenu> {
}

( 3 )权限按钮(点)持久化类

1
2
3
public interface PermissionPointDao extends JpaRepository<PermissionPoint, String>, 
JpaSpecificationExecutor<PermissionPoint> {
}

( 4 )权限API持久化类

1
2
3
public interface PermissionApiDao extends JpaRepository<PermissionApi, String>, 
JpaSpecificationExecutor<PermissionApi> {
}

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
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
package com.ihrm.system.service;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.common.exception.CommonException;
import com.ihrm.common.utils.BeanMapUtils;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.common.utils.PermissionConstants;
import com.ihrm.domain.system.*;
import com.ihrm.system.dao.*;
import com.ihrm.system.dao.PermissionDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
@Transactional
public class PermissionService {
   @Autowired
   private PermissionDao permissionDao;
   @Autowired
   private PermissionMenuDao permissionMenuDao;
   @Autowired
   private PermissionPointDao permissionPointDao;
   @Autowired
   private PermissionApiDao permissionApiDao;
   @Autowired
   private IdWorker idWorker;
   /**
    * 1.保存权限
    */
   public void save(Map<String,Object> map) throws Exception {
       //设置主键的值
       String id = idWorker.nextId()+"";
       //1.通过map构造permission对象
       Permission perm = BeanMapUtils.mapToBean(map,Permission.class);
       perm.setId(id);
       //2.根据类型构造不同的资源对象(菜单,按钮,api)
       int type = perm.getType();
       switch (type) {
           case PermissionConstants.PERMISSION_MENU:
               PermissionMenu menu = BeanMapUtils.mapToBean(map,PermissionMenu.class);
               menu.setId(id);
               permissionMenuDao.save(menu);
               break;
           case PermissionConstants.PERMISSION_POINT:
               PermissionPoint point =
BeanMapUtils.mapToBean(map,PermissionPoint.class);
               point.setId(id);
               permissionPointDao.save(point);
               break;
           case PermissionConstants.PERMISSION_API:
               PermissionApi api = BeanMapUtils.mapToBean(map,PermissionApi.class);
               api.setId(id);
               permissionApiDao.save(api);
               break;
           default:
               throw new CommonException(ResultCode.FAIL);
      }
      //3.保存
       permissionDao.save(perm);
  }
   /**
    * 2.更新权限
    */
   public void update(Map<String,Object> map) throws Exception {
       Permission perm = BeanMapUtils.mapToBean(map,Permission.class);
       //1.通过传递的权限id查询权限
       Permission permission = permissionDao.findById(perm.getId()).get();
       permission.setName(perm.getName());
       permission.setCode(perm.getCode());
       permission.setDescription(perm.getDescription());
       permission.setEnVisible(perm.getEnVisible());
       //2.根据类型构造不同的资源
       int type = perm.getType();
       switch (type) {
           case PermissionConstants.PERMISSION_MENU:
               PermissionMenu menu = BeanMapUtils.mapToBean(map,PermissionMenu.class);
               menu.setId(perm.getId());
               permissionMenuDao.save(menu);
               break;
           case PermissionConstants.PERMISSION_POINT:
               PermissionPoint point =
BeanMapUtils.mapToBean(map,PermissionPoint.class);
               point.setId(perm.getId());
               permissionPointDao.save(point);
               break;
           case PermissionConstants.PERMISSION_API:
               PermissionApi api = BeanMapUtils.mapToBean(map,PermissionApi.class);
               api.setId(perm.getId());
               permissionApiDao.save(api);
               break;
           default:
               throw new CommonException(ResultCode.FAIL);
      }
       //3.保存
       permissionDao.save(permission);
  }
   /**
    * 3.根据id查询
    *     //1.查询权限
    *     //2.根据权限的类型查询资源
    *     //3.构造map集合
    */
   public Map<String, Object> findById(String id) throws Exception {
       Permission perm = permissionDao.findById(id).get();
       int type = perm.getType();
       Object object = null;
       if(type == PermissionConstants.PERMISSION_MENU) {
           object = permissionMenuDao.findById(id).get();
      }else if (type == PermissionConstants.PERMISSION_POINT) {
           object = permissionPointDao.findById(id).get();
      }else if (type == PermissionConstants.PERMISSION_API) {
           object = permissionApiDao.findById(id).get();
      }else {
           throw new CommonException(ResultCode.FAIL);
      }
       Map<String, Object> map = BeanMapUtils.beanToMap(object);
       map.put("name",perm.getName());
       map.put("type",perm.getType());
       map.put("code",perm.getCode());
       map.put("description",perm.getDescription());
       map.put("pid",perm.getPid());
       map.put("enVisible",perm.getEnVisible());
       return map;
  }
   /**
    * 4.查询全部
    * type     : 查询全部权限列表type:0:菜单 + 按钮(权限点) 1:菜单2:按钮(权限点)3:API接 口
    * enVisible : 0:查询所有saas平台的最高权限,1:查询企业的权限
    * pid :父id
    */
   public List<Permission> findAll(Map<String, Object> map) {
       //1.需要查询条件
       Specification<Permission> spec = new Specification<Permission>() {
           /**
            * 动态拼接查询条件
            * @return
            */
           public Predicate toPredicate(Root<Permission> root, CriteriaQuery<?>
criteriaQuery, CriteriaBuilder criteriaBuilder) {
               List<Predicate> list = new ArrayList<>();
               //根据父id查询
               if(!StringUtils.isEmpty(map.get("pid"))) {
                   list.add(criteriaBuilder.equal(root.get("pid").as(String.class),
(String)map.get("pid")));
              }
               //根据enVisible查询
               if(!StringUtils.isEmpty(map.get("enVisible"))) {
                 
list.add(criteriaBuilder.equal(root.get("enVisible").as(String.class),
(String)map.get("enVisible")));
              }
               //根据类型 type
               if(!StringUtils.isEmpty(map.get("type"))) {
                String ty = (String) map.get("type");
                   CriteriaBuilder.In<Object> in =
criteriaBuilder.in(root.get("type"));
                   if("0".equals(ty)) {
                       in.value(1).value(2);
                  }else{
                       in.value(Integer.parseInt(ty));
                  }
                   list.add(in);
              }
               return criteriaBuilder.and(list.toArray(new Predicate[list.size()]));
          }
      };
       return permissionDao.findAll(spec);
  }
   /**
    * 5.根据id删除
    * //1.删除权限
    * //2.删除权限对应的资源
    *
    */
   public void deleteById(String id) throws Exception {
       //1.通过传递的权限id查询权限
       Permission permission = permissionDao.findById(id).get();
       permissionDao.delete(permission);
       //2.根据类型构造不同的资源
       int type = permission.getType();
       switch (type) {
           case PermissionConstants.PERMISSION_MENU:
               permissionMenuDao.deleteById(id);
               break;
           case PermissionConstants.PERMISSION_POINT:
               permissionPointDao.deleteById(id);
               break;
           case PermissionConstants.PERMISSION_API:
               permissionApiDao.deleteById(id);
               break;
           default:
               throw new CommonException(ResultCode.FAIL);
      }
  }
}

1.2.4 控制器

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
package com.ihrm.system.controller;
import com.ihrm.common.entity.PageResult;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.domain.system.Permission;
import com.ihrm.domain.system.User;
import com.ihrm.system.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
//1.解决跨域
@CrossOrigin
//2.声明restContoller
@RestController
//3.设置父路径
@RequestMapping(value="/sys")
public class PermissionController {
   @Autowired
   private PermissionService permissionService;
   /**
    * 保存
    */
   @RequestMapping(value = "/permission", method = RequestMethod.POST)
   public Result save(@RequestBody Map<String,Object> map) throws Exception {
       permissionService.save(map);
       return new Result(ResultCode.SUCCESS);
  }
   /**
    * 修改
    */
   @RequestMapping(value = "/permission/{id}", method = RequestMethod.PUT)
   public Result update(@PathVariable(value = "id") String id, @RequestBody
Map<String,Object> map) throws Exception {
       //构造id
       map.put("id",id);
       permissionService.update(map);
       return new Result(ResultCode.SUCCESS);
  }
   /**
    * 查询列表
    */
   @RequestMapping(value = "/permission", method = RequestMethod.GET)
   public Result findAll(@RequestParam Map map) {
       List<Permission> list =  permissionService.findAll(map);
       return new Result(ResultCode.SUCCESS,list);
  }
   /**
    * 根据ID查询
    */
   @RequestMapping(value = "/permission/{id}", method = RequestMethod.GET)
   public Result findById(@PathVariable(value = "id") String id) throws Exception {
       Map map = permissionService.findById(id);
       return new Result(ResultCode.SUCCESS,map);
  }
   /**
    * 根据id删除
    */
   @RequestMapping(value = "/permission/{id}", method = RequestMethod.DELETE)
   public Result delete(@PathVariable(value = "id") String id) throws Exception {
       permissionService.deleteById(id);
       return new Result(ResultCode.SUCCESS);
  }
}

1.3 前端实现

1.3.1 引入权限管理模块

将今日提供的资料module-permissions引入到工程的/src文件夹下,在/src/main.js完成模块注册

1
2
import permissions from '@/module-permissions/' // 权限管理
Vue.use(permissions, store)

1.3.2 配置API

在/src/api/base/目录下创建permissions.js

1
2
3
4
5
6
7
8
import {createAPI} from '@/utils/request'
const api = "/sys/permission"
export const list = data => createAPI(`${api}`, 'get', data)
export const add = data => createAPI(`${api}`, 'post', data)
export const update = data => createAPI(`${api}/${data.id}`, 'put', data)
export const remove = data => createAPI(`${api}/${data.id}`, 'delete', data)
export const detail = data => createAPI(`${api}/${data.id}`, 'get', data)
export const saveOrUpdate = data => {return data.id?update(data):add(data)}

1.3.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
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
<template>
 <div class="dashboard-container">
   <div class="app-container">
     <el-card shadow="never">
       <el-button class="filter-item fr" size="small" style="margin-left: 10px;"
@click="handleCreate(null,1);setPid(1,'0')" type="primary" icon="el-icon-edit">添加菜单
</el-button>
<el-table :data="dataList" fit style="width: 100%;" highlight-current-row>
               <el-table-column fixed prop="name" label="菜单名称" width="200px">
                   <template slot-scope="scope">
                       <i :class="scope.row.type==1?'ivu-icon fa fa-folder-open-o fafw':'ivu-icon el-icon-view'"
                           :style="scope.row.type==1?'margin-left: 0px':'margin-left:
20px'"></i>
                       <span @click="show(scope.$index,scope.row.id)">
{{scope.row.name}}</span>
                   </template>
               </el-table-column>
               <el-table-column fixed prop="code" label="权限标识" width="200"></eltable-column>
               <el-table-column fixed prop="description" label="描述" width="200"></eltable-column>        
               <el-table-column fixed="right" label="操作">
                   <template slot-scope="scope">
                       <el-button v-if="scope.row.type==1"
@click="handleCreate(null,2);setPid(2,scope.row.id)" type="text" size="small">添加权限点
</el-button>
                       <el-button @click="handlerApiList(scope.row.id)" type="text"
size="small">查看api权限</el-button>
                       <el-button
@click="handleCreate(scope.row.id,scope.row.type);setPid(scope.row.type,scope.row.pid)"
type="text" size="small">查看</el-button>
                       <el-button @click="handleDelete(scope.row.id)" type="text"
size="small">删除</el-button>
                   </template>
               </el-table-column>
           </el-table>
       </el-card>
     </div>
     <el-dialog title="编辑权限" :visible.sync="dialogFormVisible"
style="hight:100px;line-height:1px">
         <el-form :model="formData" label-width="90px" style="margin-top:20px">
           <el-form-item label="权限名称">
             <el-input v-model="formData.name" autocomplete="off" style="width:90%">
</el-input>
           </el-form-item>
           <el-form-item label="权限标识">
             <el-input v-model="formData.code" autocomplete="off" style="width:90%">
</el-input>
           </el-form-item>
           <el-form-item label="权限描述">
             <el-input v-model="formData.description" autocomplete="off"
style="width:90%"></el-input>
           </el-form-item>
           <div v-if="type==1">
             <el-form-item label="菜单顺序">
               <el-input v-model="formData.menuOrder" autocomplete="off"
style="width:90%"></el-input>
             </el-form-item>
             <el-form-item label="菜单icon">
               <el-input v-model="formData.menuIcon" autocomplete="off"
style="width:90%"></el-input>
             </el-form-item>
           </div>
           <div v-else-if="type==2">
             <el-form-item label="按钮样式">
               <el-input v-model="formData.pointClass" autocomplete="off"
style="width:90%"></el-input>
             </el-form-item>
             <el-form-item label="按钮icon">
               <el-input v-model="formData.pointIcon" autocomplete="off"
style="width:90%"></el-input>
             </el-form-item>  
             <el-form-item label="按钮状态">
               <el-input v-model="formData.pointStatus" autocomplete="off"
style="width:90%"></el-input>
             </el-form-item>
           </div>
           <div v-else-if="type==3">
             <el-form-item label="api请求地址">
               <el-input v-model="formData.apiUrl" autocomplete="off"
style="width:90%"></el-input>
             </el-form-item>
             <el-form-item label="api请求方式">
               <el-input v-model="formData.apiMethod" autocomplete="off"
style="width:90%"></el-input>
             </el-form-item>  
             <el-form-item label="api类型">
               <el-input v-model="formData.apiLevel" autocomplete="off"
style="width:90%"></el-input>
             </el-form-item>              
           </div>
         </el-form>
         <div slot="footer" class="dialog-footer">
           <el-button @click="dialogFormVisible = false">取 消</el-button>
           <el-button type="primary" @click="saveOrUpdate">确 定</el-button>
         </div>
     </el-dialog>
     <el-dialog  title="API权限列表" :visible.sync="apiDialogVisible"
style="hight:400px;line-height:1px">
           <el-button class="filter-item fr" size="small" style="margin-left: 10px;"
@click="handleCreate(null,1);setPid(3,pid)" type="primary" icon="el-icon-edit">添加api权 限</el-button>
           <el-table :data="apiList" fit style="width: 100%;" max-height="250" >
               <el-table-column fixed prop="name" label="菜单名称" width="120px"></eltable-column>
               <el-table-column fixed prop="code" label="权限标识" width="200"></eltable-column>
               <el-table-column fixed prop="description" label="描述" width="200"></eltable-column>
               <el-table-column fixed="right" label="操作" width="200">
                   <template slot-scope="scope">
                       <el-button
@click="handleCreate(scope.row.id,scope.row.type);setPid(scope.row.type,scope.row.pid)"
type="text" size="small">查看</el-button>
                       <el-button
@click="handleDelete(scope.row.id);handlerApiList(pid)" type="text" size="small">删除
</el-button>
                   </template>
               </el-table-column>
           </el-table>        
     </el-dialog>
 </div>
</template>
<script>
import {saveOrUpdate,list,detail,remove} from "@/api/base/permissions"
export default {
 name: 'permissions-table-index',
 data() {
   return {
     MenuList: 'menuList',
     type:0,
     pid:"",
     dialogFormVisible:false,
     apiDialogVisible:false,
     formData:{},
     dataList:[],
     apiList:[],
     pointEnable:{}
  }
},
 methods: {
   setPid(type,pid){
     this.pid = pid;
     this.type = type
  },
   handleCreate(id) {
     if(id && id !=undefined) {
       detail({id}).then(res => {
         this.formData = res.data.data
         this.dialogFormVisible=true
      })
    }else{
       this.formData = {}
       this.dialogFormVisible=true
    }
  },
   saveOrUpdate() {
     this.formData.type = this.type
     this.formData.pid = this.pid
     saveOrUpdate(this.formData).then(res => {
     this.$message({message:res.data.message,type:res.data.success?"success":"error"});
       if(res.data.success){
         this.formData={};
         this.dialogFormVisible=false;
      }
       if(this.type ==3){
         this.handlerApiList(this.pid);
      }else{
         this.getList();
         this.pointEnable = {}
      }
    })
  },
   handleDelete(id) {
     remove({id}).then(res=> {
     
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
    })
  },
   getList() {
     list({type:1,pid:0}).then(res=> {
         this.dataList = res.data.data
    })
  },
   show(index,id) {
       if(!this.pointEnable[id] == null || this.pointEnable[id]==undefined){
           list({type:2,pid:id}).then(res=> {
               for(var i = 0 ; i <res.data.data.length;i++) {
                   this.dataList.splice(index+1,0,res.data.data[i]);
              }
               this.pointEnable[id] = res.data.data.length;
               console.log(this.dataList)
          })
      }else{
           this.dataList.splice(index+1,this.pointEnable[id])
           this.pointEnable[id] = null;
      }
  },
   handlerApiList(id) {
     this.pid = id;
     list({type:3,pid:id}).then(res=> {
         this.apiList = res.data.data
         this.apiDialogVisible = true;
    })
  }
},
 created () {
   this.getList();
}
}

2 分配角色

2.1 需求分析

由于使用了RBAC模型对权限进行统一管理,所以每个SAAS-HRM平台的用户都应该具有角色的信息。进而通过角色完成对权限的识别。众所周知,一个用户可以具有很多的角色,一个角色可以被分配给不同的用户。所以用户和角色之间是多对多关系。

image-20220922101251327

2.2 服务端代码实现

( 1 ) 改造用户实体类,添加角色的id集合属性,表明一个用户具有的多个角色id

在com.ihrm.system.domain.User用户实体类中添加与角色的多对多关系并进行JPA的配置

1
2
3
4
5
6
@ManyToMany
@JsonIgnore
@JoinTable(name="pe_user_role",joinColumns= {@JoinColumn(name="user_id",referencedColumnName="id")},
  inverseJoinColumns={@JoinColumn(name="role_id",referencedColumnName="id")}
)
private Set<Role> roles = new HashSet<Role>();//用户与角色   多对多

在com.ihrm.system.domain.Role角色实体类中配置角色与用户的多对多关系并进行JPA配置

1
2
3
@JsonIgnore
@ManyToMany(mappedBy="roles")
private Set<User> users = new HashSet<User>(0);//角色与用户   多对多

( 2 ) 在com.ihrm.system.controller.UserController添加分配角色的控制器方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
    * 分配角色
    */
   @RequestMapping(value = "/user/assignRoles", method = RequestMethod.PUT)
   public Result assignRoles(@RequestBody Map<String,Object> map) {
       //1.获取被分配的用户id
       String userId = (String) map.get("id");
       //2.获取到角色的id列表
       List<String> roleIds = (List<String>) map.get("roleIds");
       //3.调用service完成角色分配
       userService.assignRoles(userId,roleIds);
       return new Result(ResultCode.SUCCESS);
  }

( 3 ) 业务逻辑层添加分配角色的业务方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 /**
    * 分配角色
    */
   public void assignRoles(String userId,List<String> roleIds) {
       //1.根据id查询用户
       User user = userDao.findById(userId).get();
       //2.设置用户的角色集合
       Set<Role> roles = new HashSet<>();
       for (String roleId : roleIds) {
           Role role = roleDao.findById(roleId).get();
           roles.add(role);
      }
       //设置用户和角色集合的关系
       user.setRoles(roles);
       //3.更新用户
       userDao.save(user);
  }

2.3 前端代码实现

( 1 ) \src\module-employees添加分配角色的组件

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
<template>
 <div class="add-form">
   <el-dialog title="分配角色" :visible.sync="roleFormVisible" style="height:300px">
     <el-form  :model="formBase"  label-position="left" label-width="120px"
style='margin-left:120px; width:500px;'>
         <el-checkbox-group
           v-model="checkedCities1">
           <el-checkbox v-for="(item,index) in cities" :label="item.id" :key="index">
{{item.name}}</el-checkbox>
         </el-checkbox-group>
     </el-form>
     <div slot="footer" class="dialog-footer">
       <el-button type="primary" @click="createData">提交</el-button>
       <el-button @click="roleFormVisible=false">取消</el-button>
     </div>
   </el-dialog>
 </div>
</template>
<script>
import {findAll} from "@/api/base/role"
import {assignRoles} from "@/api/base/users"
export default {
   data () {
       return {
           roleFormVisible:false,
           formBase:{},
           checkedCities1:[],
           data:[],
           cities:[],
           id:null
      }
  },
   methods: {
       toAssignPrem(id) {
           findAll().then(res => {
               this.id = id;
               this.cities = res.data.data
               this.roleFormVisible=true
          })
      },
       createData() {
           assignRoles({id:this.id,ids:this.checkedCities1}).then(res => {
             
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
               this.roleFormVisible=false
          })
      }
  }
}
</script>

( 2 )\src\module-employees\pages\employees-list.vue 引入组件

1
2
<!--分配角色组件 -->
<component v-bind:is="addRole" ref="addRole"></component>

3 分配权限

3.1 需求分析

完成对角色权限的分配。

image-20220922101353430

3.2 服务端代码实现

( 1 ) 角色实体类中添加与权限的多对多关系并进行JPA配置

1
2
3
4
5
6
@JsonIgnore //忽略json转化
@ManyToMany
@JoinTable(name="pe_role_permission",
       joinColumns={@JoinColumn(name="role_id",referencedColumnName="id")},
       inverseJoinColumns= {@JoinColumn(name="permission_id",referencedColumnName="id")})
private Set<Permission> permissions = new HashSet<Permission>(0);//角色与模块 多对多

( 2 )控制器类(com.ihrm.system.controller.RoleController)添加权限分配

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
    * 分配权限
    */
   @RequestMapping(value = "/role/assignPrem", method = RequestMethod.PUT)
   public Result assignPrem(@RequestBody Map<String,Object> map) {
       //1.获取被分配的角色的id
       String roleId = (String) map.get("id");
       //2.获取到权限的id列表
       List<String> permIds = (List<String>) map.get("permIds");
       //3.调用service完成权限分配
       roleService.assignPerms(roleId,permIds);
       return new Result(ResultCode.SUCCESS);
  }

( 3 ) 持久化类中添加分配权限方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 /**
    * 分配权限
    */
   public void assignPerms(String roleId,List<String> permIds) {
       //1.获取分配的角色对象
       Role role = roleDao.findById(roleId).get();
       //2.构造角色的权限集合
       Set<Permission> perms = new HashSet<>();
       for (String permId : permIds) {
           Permission permission = permissionDao.findById(permId).get();
           //需要根据父id和类型查询API权限列表
           List<Permission> apiList =
permissionDao.findByTypeAndPid(PermissionConstants.PERMISSION_API, permission.getId());
           perms.addAll(apiList);//自定赋予API权限
           perms.add(permission);//当前菜单或按钮的权限
      }
       System.out.println(perms.size());
       //3.设置角色和权限的关系
       role.setPermissions(perms);
       //4.更新角色
       roleDao.save(role);
  }

3.3 前端代码实现

( 1 )在\src\module-settings\components\role-list.vue绑定权限按钮

1
2
3
4
5
6
7
<el-table-column fixed="right" label="操作" align="center" width="250">
  <template slot-scope="scope">
     <el-button @click="handlerPerm(scope.row)" type="text" size="small">分配权限</elbutton>
     <el-button @click="handleUpdate(scope.row)" type="text" size="small">修改</elbutton>
     <el-button @click="handleDelete(scope.row)" type="text" size="small">删除</elbutton>
  </template>
</el-table-column>

( 2 )在\src\api\base\role.js中添加分配权限的API方法

1
export const assignPrem = data => createAPI(`/sys/role/assignPrem`, 'put', data)

( 3 )\src\module-settings\components\role-list.vue使用Element-UI构造权限树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<el-dialog :title="'为【'+formData.name+'】分配权限'" :visible.sync="permFormVisible"
style="hight:100px;line-height:1px">
     <el-tree
       :data="treeData"
       default-expand-all
       show-checkbox
       node-key="id"
       ref="tree"
       :default-checked-keys="checkNodes"
       :props="{label:'name'}">
     </el-tree>
     <div slot="footer" class="dialog-footer">
       <el-button @click="permFormVisible = false">取 消</el-button>
       <el-button type="primary" @click="assignPrem">确 定</el-button>
     </div>
   </el-dialog>

( 4 ) 完成添加权限

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
import {list,add,update,remove,detail,assignPrem} from "@/api/base/role"
import * as permApi from "@/api/base/permissions"
import commonApi from "@/utils/common"
import PageTool from './../../components/page/page-tool'
var _this = null
export default {
 name: 'roleList',
 components: {PageTool},
 props: ['objId'],
 data() {
   return {
     formData:{},
     treeData:[],
     checkNodes:[],
     dialogFormVisible: false,
     permFormVisible:false,
     dataList:[],
     counts:0,
     requestParameters:{
       page: 1,
       pagesize: 10
    }    
  }
},
 methods: {
   assignPrem() {
   
assignPrem({roleId:this.formData.id,ids:this.$refs.tree.getCheckedKeys()}).then(res =>
{
       
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
         this.permFormVisible=false
    })
  },
   handlerPerm(obj) {
      detail({id:obj.id}).then(res=>{
        this.formData = res.data.data;
        if(this.formData.menusIds != null) {
           this.checkNodes = this.formData.menusIds.split(",")
        }
        if(this.formData.pointIds != null) {
         this.checkNodes.push(this.formData.pointIds.split(","))
        }
        permApi.list({type:0,pid:null}).then(res => {
           this.treeData = commonApi.transformTozTreeFormat(res.data.data)
           this.permFormVisible=true
        })
      })
  },
   handlerAdd() {
     this.formData={}
     this.dialogFormVisible = true
  },
   handleDelete(obj) {
     this.$confirm(
       `本次操作将删除${obj.name},删除后角色将不可恢复,您确认删除吗?`
    ).then(() => {
         remove({id: obj.id}).then(res => {
           
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
             this.doQuery()
        })
    })
  },
   handleUpdate(obj) {
     detail({id:obj.id}).then(res=>{
       this.formData = res.data.data;
       this.formData.id = obj.id;
       this.dialogFormVisible = true
    })
  },
   saveOrUpdate() {
     if(this.formData.id == null || this.formData.id == undefined) {
         this.save()
    }else{
         this.update();
    }
  },
   update(){
     update(this.formData).then(res=>{
     
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
       if(res.data.success){
         this.formData={};
         this.dialogFormVisible=false;
         this.doQuery();
      }
    })
  },
   save() {
     add(this.formData).then(res=>{
     
this.$message({message:res.data.message,type:res.data.success?"success":"error"});
       if(res.data.success){
         this.formData={};
         this.dialogFormVisible=false;
          this.doQuery();
      }
    })
  },
   // 获取详情
   doQuery() {
     list(this.requestParameters).then(res => {
         this.dataList = res.data.data.rows
         this.counts = res.data.data.total
      })
  },
   // 每页显示信息条数
   handleSizeChange(pageSize) {
     this.requestParameters.pagesize = pageSize
     if (this.requestParameters.page === 1) {
       _this.doQuery(this.requestParameters)
    }
  },
   // 进入某一页
   handleCurrentChange(val) {
     this.requestParameters.page = val
     _this.doQuery()
  },
},
 // 挂载结束
 mounted: function() {},
 // 创建完毕状态
 created: function() {
   _this = this
   this.doQuery()
},
 // 组件更新
 updated: function() {}
}</script>

4 常见的认证机制

4.1 HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效

4.3 OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。

image-20220922101538448

这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。

4.4 Token Auth

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

image-20220922101557941

Token Auth的优点

  • 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
  • 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
  • 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
  • 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
  • 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  • CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  • 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
  • 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
  • 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby,Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).

5 HRM中的TOKEN签发与验证

5.1 什么是JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。在Java世界中通过JJWT实现JWT创建和验证。

5.2 JJWT的快速入门

5.2.1 token的创建

( 1 )创建maven工程,引入依赖

1
2
3
4
5
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.6.0</version>
</dependency>

( 2 )创建类CreateJwtTest,用于生成token

1
2
3
4
5
6
7
8
9
public class CreateJwtTest {
    public static void main(String[] args) {
        JwtBuilder builder= Jwts.builder().setId("888")
                .setSubject("小白")
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,"itcast");
        System.out.println( builder.compact() );
    }
}

( 3 )测试运行,输出如下:

1
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MTM0NTh9.gq0JcOM_qCNqU_s-d_IrRytaNenesPmqAIhQpYXHZk

5.2.2 token的解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

创建ParseJwtTest

1
2
3
4
5
6
7
8
9
10
11
12
public class ParseJwtTest {
    public static void main(String[] args) {
        String
token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiO
jE1MjM0MTM0NTh9.gq0J-cOM_qCNqU_s-d_IrRytaNenesPmqAIhQpYXHZk";
        Claims claims =
Jwts.parser().setSigningKey("itcast").parseClaimsJws(token).getBody();
        System.out.println("id:"+claims.getId());
        System.out.println("subject:"+claims.getSubject());
        System.out.println("IssuedAt:"+claims.getIssuedAt());
    }
}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token

5.2.3 自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims

( 1 ) 创建CreateJwtTest3,并存储指定的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CreateJwtTest3 {
    public static void main(String[] args) {
        //为了方便测试,我们将过期时间设置为1分钟
        long now = System.currentTimeMillis();//当前时间
        long exp = now + 1000*60;//过期时间为1分钟
        JwtBuilder builder= Jwts.builder().setId("888")
                .setSubject("小白")
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,"itcast")
                .setExpiration(new Date(exp))
                .claim("roles","admin") //自定义claims存储数据
                .claim("logo","logo.png");
        System.out.println( builder.compact() );
    }
}

( 2 ) 修改ParseJwtTest,获取指定信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ParseJwtTest {
    public static void main(String[] args) {
        String
compactJws="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0MT
czMjMsImV4cCI6MTUyMzQxNzM4Mywicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJsb2dvLnBuZyJ9.b11p4g4rE94r
qFhcfzdJTPCORikqP_1zJ1MP8KihYTQ";
        Claims claims =
Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJws).getBody();
        System.out.println("id:"+claims.getId());
        System.out.println("subject:"+claims.getSubject());
        System.out.println("roles:"+claims.get("roles"));
        System.out.println("logo:"+claims.get("logo"));
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        System.out.println("签发时间:"+sdf.format(claims.getIssuedAt()));
        System.out.println("过期时间:"+sdf.format(claims.getExpiration()));
        System.out.println("当前时间:"+sdf.format(new Date()) );
    }
}

5.3 JWT工具类

在ihrm_common工程中创建JwtUtil工具类

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
@ConfigurationProperties("jwt.config")
public class JwtUtil {
   private String key;
   private long ttl;
   public String getKey() {
       return key;
  }
   public void setKey(String key) {
       this.key = key;
  }
   public long getTtl() {
       return ttl;
  }
   public void setTtl(long ttl) {
       this.ttl = ttl;
  }
   /**
    * 签发 token
    */
   public String createJWT(String id, String subject,Map<String,Object> map){
       long now=System.currentTimeMillis();
       long exp=now+ttl;
       JwtBuilder jwtBuilder = Jwts.builder().setId(id)
              .setSubject(subject).setIssuedAt(new Date())
              .signWith(SignatureAlgorithm.HS256, key);
       for(Map.Entry<String,Object> entry:map.entrySet()) {
           jwtBuilder.claim(entry.getKey(),entry.getValue());
      }
       if(ttl>0){
           jwtBuilder.setExpiration( new Date(exp));
      }
       String token = jwtBuilder.compact();
       return token;
  }
   /**
   * 解析JWT
    * @param token
    * @return
    */
   public Claims parseJWT(String token){
       Claims claims = null;
       try {
           claims = Jwts.parser()
                  .setSigningKey(key)
                  .parseClaimsJws(token).getBody();
      }catch (Exception e){
      }
       return claims;
  }
}

( 3 ) 修改ihrm_common工程的application.yml, 添加配置

1
2
3
4
jwt:
 config:
    key: saas-ihrm
    ttl: 360000

5.4 登录成功签发token

( 1 )配置JwtUtil。修改ihrm_system工程的启动类

1
2
3
4
@Bean    
public JwtUtil jwtUtil(){    
return new util.JwtUtil();        
}

( 2 )添加登录方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 /**
    * 用户登录
    * 1.通过service根据mobile查询用户
    * 2.比较password
    * 3.生成jwt信息
    *
    */
   @RequestMapping(value="/login",method = RequestMethod.POST)
   public Result login(@RequestBody Map<String,String> loginMap) {
       String mobile = loginMap.get("mobile");
       String password = loginMap.get("password");
       User user = userService.findByMobile(mobile);
       //登录失败
       if(user == null || !user.getPassword().equals(password)) {
           return new Result(ResultCode.MOBILEORPASSWORDERROR);
      }else {
      //登录成功
           Map<String,Object> map = new HashMap<>();
           map.put("companyId",user.getCompanyId());
           map.put("companyName",user.getCompanyName());
           String token = jwtUtils.createJwt(user.getId(), user.getUsername(), map);
           return new Result(ResultCode.SUCCESS,token);
      }
  }

( 3 )测试运行结果

image-20220922101839370

使用postman验证登录返回:

1
2
3
4
{"success":true,"code":10000,"message":"操作成
功!","data":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDYyNjYxODkxNjE4Mzc3NzI4Iiwic3ViIjoiemhhb
mdzYW4iLCJpYXQiOjE1NDI0NjgzNzcsImNvbXBhbnlJZCI6IjEiLCJjb21wYW55TmFtZSI6IuS8oOaZuuaSreWu
oiIsImV4cCI6MTU0MjU1NDc3N30.J-8uv8jOp2GMLpBwrUOksnErjA4-DOJ_qvy7tsJbsa8"}

5.5 获取用户信息鉴权

需求:用户登录成功之后,会发送一个新的请求到服务端,获取用户的详细信息。获取用户信息的过程中必须登录才能,否则不能获取。

前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格+token

( 1 )添加响应值对象

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
@Getter
@Setter
@NoArgsConstructor
public class ProfileResult {
   private String mobile;
   private String username;
   private String company;
   private Map roles;
   public ProfileResult(User user) {
       this.mobile = user.getMobile();
       this.username = user.getUsername();
       this.company = user.getCompanyName();
       //角色数据
       Set<String> menus = new HashSet<>();
       Set<String> points = new HashSet<>();
       Set<String> apis = new HashSet<>();
       Map rolesMap = new HashMap<>();
       for (Role role : user.getRoles()) {
           for (Permission perm : role.getPermissions()) {
               String code = perm.getCode();
               if(perm.getType() == 1) {
                   menus.add(code);
              }else if(perm.getType() == 2) {
                   points.add(code);
              }else {
                   apis.add(code);
              }
          }
      }
       rolesMap.put("menus",menus);
       rolesMap.put("points",points);
       rolesMap.put("apis",points);
       this.roles = rolesMap;
  }
  }

( 2 )添加profile方法

1
2
3
4
5
6
7
8
9
10
/**
* 获取个人信息
*/
@RequestMapping(value = "/profile", method = RequestMethod.POST)
public Result profile(HttpServletRequest request) throws Exception {
//临时使用
   String userId = "1";
   User user = userService.findById(userId);
   return new Result(ResultCode.SUCCESS,new ProfileResult(user));
}

( 3 )验证token

思路:从请求中获取key为Authorization的token信息,并使用jwt验证,验证成功后获取隐藏信息。

修改profile方法添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping(value = "/profile", method = RequestMethod.POST)
public Result profile(HttpServletRequest request) throws Exception {
   //请求中获取key为Authorization的头信息
    String authorization = request.getHeader("Authorization");
   if(StringUtils.isEmpty(authorization)) {
       throw new CommonException(ResultCode.UNAUTHENTICATED);
  }
   //前后端约定头信息内容以 Bearer+空格+token 形式组成
   String token = authorization.replace("Bearer ", "");
   //比较并获取claims
   Claims claims = jwtUtil.parseJWT(token);
   if(claims == null) {
       throw new CommonException(ResultCode.UNAUTHENTICATED);
  }
   String userId = claims.getId();
   User user = userService.findById(userId);
   return new Result(ResultCode.SUCCESS,new ProfileResult(user));
}

第 5 章 权限管理与Shiro入门

学习目标:

  • 理解前端权限控制思路

  • 理解有状态服务和无状态服务

  • 通过拦截器实现JWT鉴权

  • 能够理解shiro以及shiro的认证和授权

  • 1 前端权限控制

1.1 需求分析

1.1.1 需求说明

基于前后端分离的开发模式中,权限控制分为前端页面可见性权限与后端API接口可访问行权限。前端的权限控制主要围绕在菜单是否可见,以及菜单中按钮是否可见两方面展开的。

1.1.2 实现思路

在vue工程中,菜单可以简单的理解为vue中的路由,只需要根据登录用户的权限信息动态的加载路由列表就可以动态的构造出访问菜单。

  1. 登录成功后获取用户信息,包含权限列表(菜单权限,按钮权限)

  2. 根据用户菜单权限列表,动态构造路由(根据路由名称和权限标识比较)

  3. 页面按钮权限通过自定义方法控制可见性

image-20220922102021390

1.2 服务端代码实现

对系统微服务的FrameController的profile方法(获取用户信息接口)进行修改,添加权限信息

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
 /**
    * 获取个人信息
    */
   @RequestMapping(value = "/profile", method = RequestMethod.POST)
   public Result profile(HttpServletRequest request) throws Exception {
       //请求中获取key为Authorization的头信息
       String authorization = request.getHeader("Authorization");
       if(StringUtils.isEmpty(authorization)) {
           throw new CommonException(ResultCode.UNAUTHENTICATED);
      }
       //前后端约定头信息内容以 Bearer+空格+token 形式组成
       String token = authorization.replace("Bearer ", "");
       //比较并获取claims
       Claims claims = jwtUtil.parseJWT(token);
       if(claims == null) {
       throw new CommonException(ResultCode.UNAUTHENTICATED);
      }
       //查询用户
       User user = userService.findById(userId);
       ProfileResult result = null;
       if("user".equals(user.getLevel())) {
           result = new ProfileResult(user);
      }else {
           Map map = new HashMap();
           if("coAdmin".equals(user.getLevel())) {
               map.put("enVisible","1");
          }
           List<Permission> list = permissionService.findAll(map);
           result = new ProfileResult(user,list);
      }
       return new Result(ResultCode.SUCCESS,result);
  }

1.3 前端代码实现

1.3.1 路由钩子函数

vue路由提供的钩子函数(beforeEach)主要用来在加载之前拦截导航,让它完成跳转或取消。可以在路由钩子函数中进行校验是否对某个路由具有访问权限

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
router.beforeEach((to, from, next) => {
 NProgress.start() // start progress bar
 if (getToken()) {
   // determine if there has token
   /* has token */
   if (to.path === '/login') {
     next({path: '/'})
     NProgress.done() // if current page is dashboard will not trigger afterEach hook,
so manually handle it
  } else {
     if (store.getters.roles.length === 0) {
       // 判断当前用户是否已拉取完user_info信息
       store
        .dispatch('GetUserInfo')
        .then(res => {
           // 拉取user_info
           const roles = res.data.data.roles // note: roles must be a array! such as:
['editor','develop']
           store.dispatch('GenerateRoutes', {roles}).then(() => {
             // 根据roles权限生成可访问的路由表
             router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
             next({...to, replace: true}) // hack方法 确保addRoutes已完成 ,set the
replace: true so the navigation will not leave a history record
          })
        })
        .catch(() => {
        store.dispatch('FedLogOut').then(() => {
             Message.error('验证失败, 请重新登录')
             next({path: '/login'})
          })
        })
    } else {
       next()
    }
  }
} else {
   /* has no token */
   if (whiteList.indexOf(to.path) !== -1) {
     // 在免登录白名单,直接进入
     next()
  } else {
     next('/login') // 否则全部重定向到登录页
     NProgress.done() // if current page is login will not trigger afterEach hook, so
manually handle it
  }
}
})

1.3.2 配置菜单权限

在\src\module-dashboard\store\permission.js下进行修改,开启路由配置

1
2
3
4
5
6
7
8
9
10
11
12
actions: {
   GenerateRoutes({ commit }, data) {
     return new Promise(resolve => {
       const { roles } = data
       //动态构造权限列表
       let accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
       commit('SET_ROUTERS', accessedRouters)
       //commit('SET_ROUTERS', asyncRouterMap) // 调试开启全部路由
       resolve()
    })
  }
}

1.3.3 配置验证权限的方法

找到\src\utils\permission.js配置验证是否具有权限的验证方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 import store from '@/store'
// 检查是否有权限
export function hasPermission(roles, route) {
 if (roles.menus && route.name) {
   return roles.menus.some(role => {
     return route.name.toLowerCase() === role.toLowerCase()
  })
} else {
return false
}
}
// 检查是否有权限点
export function hasPermissionPoint(point) {
 let points = store.getters.roles.points
 if (points) {
   return points.some(it => it.toLowerCase() === point.toLowerCase())
} else {
   return false
}
}

1.3.4 修改登录和获取信息的请求接口

( 1 )关闭模拟测试接口

\mock\index.js中不加载登录(login)以及(profile)的模拟测试

1
2
3
4
5
6
7
8
9
10
import Mock from 'mockjs'
import TableAPI from './table'
import ProfileAPI from './profile'
import LoginAPI from './login'
Mock.setup({
 //timeout: '1000'
})
Mock.mock(/\/table\/list\.*/, 'get', TableAPI.list)
//Mock.mock(/\/frame\/profile/, 'post', ProfileAPI.profile)
//Mock.mock(/\/frame\/login/, 'post', LoginAPI.login)

1.4 权限测试

( 1 ) 菜单测试

分配好权限之后,重新登录前端页面,左侧菜单已经发生了变化。

( 2 )对需要进行权限控制的(权限点)验证测试

页面添加校验方法

1
<el-button  type="primary" v-if="checkPoint('POINT-USER-ADD')" size="mini" icon="elicon-plus" @click="handlAdd">新增员工</el-button>

2 有状态服务和无状态服务

2.1 什么是服务中的状态

有状态和无状态服务是两种不同的服务架构,两者的不同之处在于对于服务状态的处理。服务状态是服务请求所需的数据,它可以是一个变量或者一个数据结构。无状态服务不会记录服务状态,不同请求之间也是没有任何关系;而有状态服务则反之。对服务器程序来说,究竟是有状态服务,还是无状态服务,其判断依据——两个来自相同发起者的请求在服务器端是否具备上下文关系。

2.2 无状态服务

无状态请求,服务器端所能够处理的数据全部来自于请求所携带的信息,无状态服务对于客户端的单次请求的处理,不依赖于其他请求,处理一次请求的信息都包含在该请求里。最典型的就是通过cookie保存token的方式传输请求数据。也可以理解为Cookie是通过客户端保持状态的解决方案。

image-20220922102300640

2.3 有状态服务

有状态服务则相反,服务会存储请求上下文相关的数据信息,先后的请求是可以有关联的。例如,在Web 应用中,经常会使用Session 来维系登录用户的上下文信息。虽然http 协议是无状态的,但是借助Session,可以使http 服务转换为有状态服务

image-20220922102306923

3 基于JWT的API鉴权

3.1 基于拦截器的token与鉴权

如果我们每个方法都去写一段代码,冗余度太高,不利于维护,那如何做使我们的代码看起来更清爽呢?我们可以将这段代码放入拦截器去实现

3.1.1 Spring中的拦截器

Spring为我们提供了org.springframework.web.servlet.handler.HandlerInterceptorAdapter这个适配器,继承此类,可以非常方便的实现自己的拦截器。他有三个方法:分别实现预处理、后处理(调用了Service并返回ModelAndView,但未进行页面渲染)、返回处理(已经渲染了页面)

1.在preHandle中,可以进行编码、安全控制等处理; 2.在postHandle中,有机会修改ModelAndView; 3.在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录。

3.2 签发用户API权限

在系统微服务的com.ihrm.system.controller.UserController修改签发token的登录服务添加API权限

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
/**
    * 用户登录
    * 1.通过service根据mobile查询用户
    * 2.比较password
    * 3.生成jwt信息
    *
    */
   @RequestMapping(value="/login",method = RequestMethod.POST)
   public Result login(@RequestBody Map<String,String> loginMap) {
    String mobile = loginMap.get("mobile");
       String password = loginMap.get("password");
       User user = userService.findByMobile(mobile);
       //登录失败
       if(user == null || !user.getPassword().equals(password)) {
           return new Result(ResultCode.MOBILEORPASSWORDERROR);
      }else {
           //登录成功
           //api权限字符串
           StringBuilder sb = new StringBuilder();
           //获取到所有的可访问API权限
           for (Role role : user.getRoles()) {
               for (Permission perm : role.getPermissions()) {
                   if(perm.getType() == PermissionConstants.PERMISSION_API) {
                       sb.append(perm.getCode()).append(",");
                  }
              }
          }
           Map<String,Object> map = new HashMap<>();
           map.put("apis",sb.toString());//可访问的api权限字符串
           map.put("companyId",user.getCompanyId());
           map.put("companyName",user.getCompanyName());
           String token = jwtUtils.createJwt(user.getId(), user.getUsername(), map);
           return new Result(ResultCode.SUCCESS,token);
      }
  }

3.3 拦截器中鉴权

( 1 )在ihrm-common下添加拦截器 JwtInterceptor

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
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {
   @Autowired    
   private JwtUtil jwtUtil;    
   
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
       // 1.通过request获取请求token信息
       String authorization = request.getHeader("Authorization");
       //判断请求头信息是否为空,或者是否已Bearer开头
       if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")) {
           //获取token数据
           String token = authorization.replace("Bearer ","");
           //解析token获取claims
           Claims claims = jwtUtils.parseJwt(token);
           if(claims != null) {
               //通过claims获取到当前用户的可访问API权限字符串
               String apis = (String) claims.get("apis");  //api-user-delete,api-userupdate
               //通过handler
               HandlerMethod h = (HandlerMethod) handler;
               //获取接口上的reqeustmapping注解
               RequestMapping annotation = h.getMethodAnnotation(RequestMapping.class);
               //获取当前请求接口中的name属性
               String name = annotation.name();
               //判断当前用户是否具有响应的请求权限
               if(apis.contains(name)) {
                   request.setAttribute("user_claims",claims);
                   return true;
              }else {
                   throw new CommonException(ResultCode.UNAUTHORISE);
              }
          }
      }
       throw new CommonException(ResultCode.UNAUTHENTICATED);
  }
}

( 2 )修改UserController的profile方法

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
/**
    * 用户登录成功之后,获取用户信息
    *     1.获取用户id
    *     2.根据用户id查询用户
    *     3.构建返回值对象
    *     4.响应
    */
   @RequestMapping(value="/profile",method = RequestMethod.POST)
   public Result profile(HttpServletRequest request) throws Exception {
       String userid = claims.getId();
       //获取用户信息
       User user = userService.findById(userid);
       //根据不同的用户级别获取用户权限
       ProfileResult result = null;
       if("user".equals(user.getLevel())) {
           result = new ProfileResult(user);
      }else {
           Map map = new HashMap();
           if("coAdmin".equals(user.getLevel())) {
               map.put("enVisible","1");
          }
           List<Permission> list = permissionService.findAll(map);
           result = new ProfileResult(user,list);
      }
       return new Result(ResultCode.SUCCESS,result);
  }

( 3 )配置拦截器类,创建com.ihrm.system.SystemConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.ihrm.system;
import com.ihrm.common.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class SystemConfig extends WebMvcConfigurationSupport {
   @Autowired
   private JwtInterceptor jwtInterceptor;
   @Override
   public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(jwtInterceptor).
       addPathPatterns("/**").
       excludePathPatterns("/frame/login","/frame/register/**"); //设置不拦截的请求地址
  }
}

4 Shiro安全框架

4.1 什么是Shiro

4.1.1 什么是Shiro

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

Apache Shiro 的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架应该尽可能掩盖复杂的地方,露出一个干净而直观的 API,来简化开发人员在使他们的应用程序安全上的努力。以下是你可以用 Apache Shiro 所做的事情:

  • 验证用户来核实他们的身份

  • 对用户执行访问控制,如:

    判断用户是否被分配了一个确定的安全角色

    判断用户是否被允许做某事

  • 在任何环境下使用 Session API,即使没有 Web 或 EJB 容器。

  • 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应。

  • 聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”。

  • 启用单点登录(SSO)功能。

  • 为没有关联到登录的用户启用”Remember Me”服务

4.1.2 与Spring Security的对比

Shiro:

Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。

  1. 易于理解的 Java Security API;

  2. 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);

  3. 对角色的简单的签权(访问控制),支持细粒度的签权;

  4. 支持一级缓存,以提升应用程序的性能;

  5. 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;

  6. 异构客户端会话访问;

  7. 非常简单的加密 API;

  8. 不跟任何的框架或者容器捆绑,可以独立运行

Spring Security:

除了不能脱离Spring,shiro的功能它都有。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高。

4.1.3 Shiro的功能模块

Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,而且Shiro的API也是非常简单;其基本功能点如下图所示:

image-20220922102545267

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情。
  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的。
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
  • Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
  • Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
  • Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
  • “Run As”:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
  • “Remember Me”:记住我。

4.2 Shiro的内部结构

image-20220922102607146

  • Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
  • SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
  • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  • Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • Realm:可以有 1 个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
  • SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;
  • SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的
  • Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
  • Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

4.3 应用程序使用Shiro

image-20220922102642161

也就是说对于我们而言,最简单的一个Shiro应用:

1 、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2 、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。

4.4 Shiro的入门

4.4.1 搭建基于ini的运行环境

( 1 )创建工程导入shiro坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
 <dependencies>
       <dependency>
           <groupId>org.apache.shiro</groupId>
           <artifactId>shiro-core</artifactId>
           <version>1.3.2</version>
       </dependency>
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>4.12</version>
           <scope>test</scope>
       </dependency>
   </dependencies>

4.4.1 用户认证

认证:身份认证/登录,验证用户是不是拥有相应的身份。基于shiro的认证,是通过subject的login方法完成用户

认证工作的

( 1 )在resource目录下创建shiro的ini配置文件构造模拟数据(shiro-auth.ini)

1
2
3
4
5
[users]
#模拟从数据库查询的用户
#数据格式   用户名=密码
zhangsan=123456
lisi=654321

( 2 )测试用户认证

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
 @Test
   public void testLogin() throws Exception{
       //1.加载ini配置文件创建SecurityManager
       Factory<SecurityManager> factory = new
IniSecurityManagerFactory("classpath:shiro.ini");
       //2.获取securityManager
       SecurityManager securityManager = factory.getInstance();
       //3.将securityManager绑定到当前运行环境
       SecurityUtils.setSecurityManager(securityManager);
       //4.创建主体(此时的主体还为经过认证)
       Subject subject = SecurityUtils.getSubject();
       /**
        * 模拟登录,和传统等不同的是需要使用主体进行登录
        */
       //5.构造主体登录的凭证(即用户名/密码)
       //第一个参数:登录用户名,第二个参数:登录密码
       UsernamePasswordToken upToken = new UsernamePasswordToken("zhangsan","123456");
       //6.主体登录
       subject.login(upToken);
       //7.验证是否登录成功
       System.out.println("用户登录成功="+subject.isAuthenticated());
       //8.登录成功获取数据
       //getPrincipal 获取登录成功的安全数据
       System.out.println(subject.getPrincipal());
  }

4.4.2 用户授权

授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用

户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限

( 1 )在resource目录下创建shiro的ini配置文件构造模拟数据(shiro-prem.ini)

1
2
3
4
5
6
7
8
9
10
11
[users]
#模拟从数据库查询的用户
#数据格式   用户名=密码,角色1,角色2..
zhangsan=123456,role1,role2
lisi=654321,role2
[roles]
#模拟从数据库查询的角色和权限列表
#数据格式   角色名=权限1,权限2
role1=user:save,user:update
role2=user:update,user.delete
role3=user.find

( 2 )完成用户授权

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
package cn.itcast.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;
public class ShiroTest1 {
   @Test
   public void testLogin() throws Exception{
       //1.加载ini配置文件创建SecurityManager
       Factory<SecurityManager> factory = new
IniSecurityManagerFactory("classpath:shiro.ini");
       //2.获取securityManager
       SecurityManager securityManager = factory.getInstance();
       //3.将securityManager绑定到当前运行环境
       SecurityUtils.setSecurityManager(securityManager);
       //4.创建主体(此时的主体还为经过认证)
       Subject subject = SecurityUtils.getSubject();
       /**
        * 模拟登录,和传统等不同的是需要使用主体进行登录
        */
       //5.构造主体登录的凭证(即用户名/密码)
       //第一个参数:登录用户名,第二个参数:登录密码
       UsernamePasswordToken upToken = new UsernamePasswordToken("lisi","654321");
       //6.主体登录
       subject.login(upToken);
       //7.用户认证成功之后才可以完成授权工作
       boolean hasPerm = subject.isPermitted("user:save");
       System.out.println("用户是否具有save权限="+hasPerm);
       }
}

4.4.3 自定义Realm

Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源

( 1 )自定义Realm

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
package cn.itcast.shiro;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义realm,需要继承AuthorizingRealm父类
*     重写父类中的两个方法
*         doGetAuthorizationInfo     :授权
*         doGetAuthenticationInfo     :认证
*/
public class PermissionRealm extends AuthorizingRealm {
   @Override
   public void setName(String name) {
       super.setName("permissionRealm");
  }
   /**
    * 授权:授权的主要目的就是查询数据库获取用户的所有角色和权限信息
    */
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection
principalCollection) {
       // 1.从principals获取已认证用户的信息
       String username = (String) principalCollection.getPrimaryPrincipal();
       /**
        * 正式系统:应该从数据库中根据用户名或者id查询
        *         这里为了方便演示,手动构造
        */
       // 2.模拟从数据库中查询的用户所有权限
       List<String> permissions = new ArrayList<String>();
       permissions.add("user:save");// 用户的创建
       permissions.add("user:update");// 商品添加权限
        // 3.模拟从数据库中查询的用户所有角色
       List<String> roles = new ArrayList<String>();
       roles.add("role1");
       roles.add("role2");
       // 4.构造权限数据
       SimpleAuthorizationInfo simpleAuthorizationInfo = new
SimpleAuthorizationInfo();
       // 5.将查询的权限数据保存到simpleAuthorizationInfo
       simpleAuthorizationInfo.addStringPermissions(permissions);
       // 6.将查询的角色数据保存到simpleAuthorizationInfo
       simpleAuthorizationInfo.addRoles(roles);
       return simpleAuthorizationInfo;
  }
   /**
    * 认证:认证的主要目的,比较用户输入的用户名密码是否和数据库中的一致
    */
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken
authenticationToken) throws AuthenticationException {
       //1.获取登录的upToken
       UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
       //2.获取输入的用户名密码
       String username = upToken.getUsername();
       String password = new String(upToken.getPassword());
       /**
        * 3.验证用户名密码是否正确
        * 正式系统:应该从数据库中查询用户并比较密码是否一致
        *         为了测试,只要输入的密码为123456则登录成功
        */
       if(!password.equals("123456")) {
           throw  new RuntimeException("用户名或密码错误");//抛出异常表示认证失败
      }else{
           SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,
password,
                   this.getName());
           return info;
      }
  }
}

( 2 )配置shiro的ini配置文件(shiro-realm.ini)

1
2
3
4
5
[main]
#声明realm
permReam=cn.itcast.shiro.PermissionRealm
#注册realm到securityManager中
securityManager.realms=$permReam

( 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
package cn.itcast.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Before;
import org.junit.Test;
public class ShiroTest2 {
   private SecurityManager securityManager;
   @Before
   public void init() throws Exception{
       //1.加载ini配置文件创建SecurityManager
       Factory<SecurityManager> factory = new
IniSecurityManagerFactory("classpath:shiro-realm.ini");
       //2.获取securityManager
       SecurityManager securityManager = factory.getInstance();
       //13.将securityManager绑定到当前运行环境
       SecurityUtils.setSecurityManager(securityManager);
  }
   @Test
   public void testLogin() throws Exception{
       //1.创建主体(此时的主体还为经过认证)
       Subject subject = SecurityUtils.getSubject();
       //2.构造主体登录的凭证(即用户名/密码)
       UsernamePasswordToken upToken = new UsernamePasswordToken("lisi","123456");
       //3.主体登录
       subject.login(upToken);
       //登录成功验证是否具有role1角色
       //System.out.println("当前用户具有role1="+subject.hasRole("role3"));
       //登录成功验证是否具有某些权限
       System.out.println("当前用户具有user:save权限="+subject.isPermitted("user:save"));
  }
}

4.4.4 认证与授权的执行流程分析

( 1 )认证流程

image-20220922102903079

  1. 首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;

  2. SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;

  3. Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;

  4. Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证;

  5. Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。

( 2 )授权流程

image-20220922102930839

  1. 首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer;

  2. Authorizer是真正的授权者,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;

  3. 在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;

  4. Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted/hasRole会返回true,否则返回false表示授权失败。

第 6 章 Shiro高级及SaaS-HRM的认证授权

1 Shiro在SpringBoot工程的应用

Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。越来越多的企业使用Shiro作为项目的安全框架,保证项目的平稳运行。

在之前的讲解中只是单独的使用shiro,方便学员对shiro有一个直观且清晰的认知,我们今天就来看一下shiro在springBoot工程中如何使用以及其他特性

1.1 案例说明

使用springBoot构建应用程序,整合shiro框架完成用户认证与授权。

1.1.1 数据库表

image-20220922103010064

1.1.2 基本工程结构

导入资料中准备的基本工程代码,此工程中实现了基本用户角色权限的操作。我们只需要在此工程中添加Shiro相关的操作代码即可

1.2 整合Shiro

1.2.1 spring和shiro的整合依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.3.2</version>
</dependency>

1.2.2 修改登录方法

认证:身份认证/登录,验证用户是不是拥有相应的身份。基于shiro的认证,shiro需要采集到用户登录数据使用

subject的login方法进入realm完成认证工作。

1
2
3
4
5
6
7
8
9
10
11
12
 @RequestMapping(value="/login")
   public String login(String username,String password) {
       try{
           Subject subject = SecurityUtils.getSubject();
           UsernamePasswordToken uptoken = new
UsernamePasswordToken(username,password);
           subject.login(uptoken);
           return "登录成功";
      }catch (Exception e) {
           return "用户名或密码错误";
      }
  }

1.2.3 自定义realm

Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行

验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源

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
public class CustomRealm extends AuthorizingRealm {
   @Override
   public void setName(String name) {
       super.setName("customRealm");
  }
   @Autowired
   private UserService userService;
   /**
    * 构造授权方法
    */
   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection
principalCollection) {
 //1.获取认证的用户数据
       User user = (User)principalCollection.getPrimaryPrincipal();
       //2.构造认证数据
       SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
       Set<Role> roles = user.getRoles();
       for (Role role : roles) {
           //添加角色信息
           info.addRole(role.getName());
           for (Permission permission:role.getPermissions()) {
               //添加权限信息
               info.addStringPermission(permission.getCode());
          }
      }
       return info;
  }
   /**
    * 认证方法
    */
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken
authenticationToken) throws AuthenticationException {
       //1.获取登录的upToken
       UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
       //2.获取输入的用户名密码
       String username = upToken.getUsername();
       String password = new String(upToken.getPassword());
       //3.数据库查询用户
       User user = userService.findByName(username);
       //4.用户存在并且密码匹配存储用户数据
       if(user != null && user.getPassword().equals(password)) {
           return new
SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
      }else {
           //返回null会抛出异常,表明用户不存在或密码不匹配
           return null;
      }
  }
}

1.3 Shiro的配置

SecurityManager 是 Shiro 架构的心脏,用于协调内部的多个组件完成全部认证授权的过程。例如通过调用realm完成认证与登录。使用基于springboot的配置方式完成SecurityManager,Realm的装配

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
@Configuration
public class ShiroConfiguration {
   //配置自定义的Realm
   @Bean
   public CustomRealm getRealm() {
       return new CustomRealm();
  }
  //配置安全管理器
   @Bean
   public SecurityManager securityManager(CustomRealm realm) {
       //使用默认的安全管理器
       DefaultWebSecurityManager securityManager = new
DefaultWebSecurityManager(realm);
       //将自定义的realm交给安全管理器统一调度管理
       securityManager.setRealm(realm);
       return securityManager;
  }
   //Filter工厂,设置对应的过滤条件和跳转条件
   @Bean
   public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
       //1.创建shiro过滤器工厂
       ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
       //2.设置安全管理器
       filterFactory.setSecurityManager(securityManager);
       //3.通用配置(配置登录页面,登录成功页面,验证未成功页面)
       filterFactory.setLoginUrl("/autherror?code=1"); //设置登录页面
       filterFactory.setUnauthorizedUrl("/autherror?code=2"); //授权失败跳转页面
       //4.配置过滤器集合
       /**
        * key :访问连接
        *     支持通配符的形式
        * value:过滤器类型
        *     shiro常用过滤器
        *         anno   :匿名访问(表明此链接所有人可以访问)
        *         authc   :认证后访问(表明此链接需登录认证成功之后可以访问)
        */
       Map<String,String> filterMap = new LinkedHashMap<String,String>();
       // 配置不会被拦截的链接 顺序判断
       filterMap.put("/user/home", "anon");
       filterMap.put("/user/**", "authc");
       //5.设置过滤器
       filterFactory.setFilterChainDefinitionMap(filterMap);
       return filterFactory;
  }
   //配置shiro注解支持
   @Bean
   public AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
       AuthorizationAttributeSourceAdvisor advisor = new
AuthorizationAttributeSourceAdvisor();
       advisor.setSecurityManager(securityManager);
       return advisor;
  }
}

1.4 shiro中的过滤器

image-20220922103156303

注意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url )

1.5 授权

授权:即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情

shiro支持基于过滤器的授权方式也支持注解的授权方式

1.5.1 基于配置的授权

在shiro中可以使用过滤器的方式配置目标地址的请求权限

1
2
3
4
5
6
7
8
9
//配置请求连接过滤器配置
       //匿名访问(所有人员可以使用)
       filterMap.put("/user/home", "anon");
       //具有指定权限访问
       filterMap.put("/user/find", "perms[user-find]");
       //认证之后访问(登录之后可以访问)
       filterMap.put("/user/**", "authc");
       //具有指定角色可以访问
       filterMap.put("/user/**", "roles[系统管理员]");

基于配置的方式进行授权,一旦操作用户不具备操作权限,目标地址不会被执行。会跳转到指定的url连接地

址。所以需要在连接地址中更加友好的处理未授权的信息提示

1.5.2 基于注解的授权

( 1 )RequiresPermissions

配置到方法上,表明执行此方法必须具有指定的权限

1
2
3
4
5
//查询
   @RequiresPermissions(value = "user-find")
   public String find() {
       return "查询用户成功";
  }

( 2 )RequiresRoles

配置到方法上,表明执行此方法必须具有指定的角色

1
2
3
4
5
 //查询
   @RequiresRoles(value = "系统管理员")
   public String find() {
       return "查询用户成功";
  }

基于注解的配置方式进行授权,一旦操作用户不具备操作权限,目标方法不会被执行,而且会抛出AuthorizationException 异常。所以需要做好统一异常处理完成未授权处理

2 Shiro中的会话管理

在shiro里所有的用户的会话信息都会由Shiro来进行控制,shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。通过Shiro的会话管理器(SessionManager)进行统一的会话管理

2.1 什么是shiro的会话管理

SessionManager(会话管理器):管理所有Subject的session包括创建、维护、删除、失效、验证等工作。

SessionManager是顶层组件,由SecurityManager管理

shiro提供了三个默认实现:

  1. DefaultSessionManager:用于JavaSE环境

  2. ServletContainerSessionManager:用于Web环境,直接使用servlet容器的会话。

  3. DefaultWebSessionManager:用于web环境,自己维护会话(自己维护着会话,直接废弃了Servlet容器的会话管理)。

在web程序中,通过shiro的Subject.login()方法登录成功后,用户的认证信息实际上是保存在HttpSession中的通过如下代码验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//登录成功后,打印所有session内容
@RequestMapping(value="/show")
 public String show(HttpSession session) {
       // 获取session中所有的键值
       Enumeration<?> enumeration = session.getAttributeNames();
       // 遍历enumeration中的
       while (enumeration.hasMoreElements()) {
           // 获取session键值
           String name = enumeration.nextElement().toString();
           // 根据键值取session中的值
           Object value = session.getAttribute(name);
           // 打印结果
           System.out.println("<B>" + name + "</B>=" + value + "<br>/n");
      }
       return "查看session成功";
  }

2.2 应用场景分析

在分布式系统或者微服务架构下,都是通过统一的认证中心进行用户认证。如果使用默认会话管理,用户信息只会保存到一台服务器上。那么其他服务就需要进行会话的同步。

image-20220922103335462

会话管理器可以指定sessionId的生成以及获取方式。

通过sessionDao完成模拟session存入,取出等操作

2.3 Shiro结合redis的统一会话管理

2.3.1 步骤分析

image-20220922103345321

2.3.2 构建环境

( 1 )使用开源组件Shiro-Redis可以方便的构建shiro与redis的整合工程。

1
2
3
4
5
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.0.0</version>
</dependency>

( 2 ) 在springboot配置文件中添加redis配置

1
2
3
redis:
host: 127.0.0.
port: 6379

2.3.3 自定义shiro会话管理器

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
/**
* 自定义的sessionManager
*/
public class CustomSessionManager extends DefaultWebSessionManager {
   /**
    * 头信息中具有sessionid
    *     请求头:Authorization: sessionid
    *
    * 指定sessionId的获取方式
    */
     protected Serializable getSessionId(ServletRequest request, ServletResponse
response) {
       //获取请求头Authorization中的数据
       String id = WebUtils.toHttp(request).getHeader("Authorization");
       if(StringUtils.isEmpty(id)) {
           //如果没有携带,生成新的sessionId
           return super.getSessionId(request,response);
      }else{
           //返回sessionId;
           request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
"header");
           request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
         
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,
Boolean.TRUE);
           return id;
      }
  }
}

2.3.4 配置Shiro基于redis的会话管理

在Shiro配置类cn.itcast.shiro.ShiroConfiguration配置

  1. 配置shiro的RedisManager,通过shiro-redis包提供的RedisManager统一对redis操作
1
2
3
4
5
6
7
8
9
10
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
//配置shiro redisManager
public RedisManager redisManager() {
   RedisManager redisManager = new RedisManager();
   redisManager.setHost(host);
   redisManager.setPort(port);
   return redisManager; }
  1. Shiro内部有自己的本地缓存机制,为了更加统一方便管理,全部替换redis实现
1
2
3
4
5
6
//配置Shiro的缓存管理器
//使用redis实现
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
r
  1. 配置SessionDao,使用shiro-redis实现的基于redis的sessionDao
1
2
3
4
5
6
7
8
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
public RedisSessionDAO redisSessionDAO() {
   RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
   redisSessionDAO.setRedisManager(redisManager());
   return redisSessionDAO; }
  1. 配置会话管理器,指定sessionDao的依赖关系
1
2
3
4
5
6
7
8
/**
    * 3.会话管理器
    */
   public DefaultWebSessionManager sessionManager() {
       CustomSessionManager sessionManager = new CustomSessionManager();
       sessionManager.setSessionDAO(redisSessionDAO());
       return sessionManager;
  }
  1. 统一交给SecurityManager管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//配置安全管理器
   @Bean
   public SecurityManager securityManager(CustomRealm realm) {
       //使用默认的安全管理器
       DefaultWebSecurityManager securityManager = new
DefaultWebSecurityManager(realm);
       // 自定义session管理 使用redis
       securityManager.setSessionManager(sessionManager());
       // 自定义缓存实现 使用redis
       securityManager.setCacheManager(cacheManager());
       //将自定义的realm交给安全管理器统一调度管理
       securityManager.setRealm(realm);
       return securityManager;
  }

3 SaaS-HRM中的认证授权

3.1 需求分析

实现基于Shiro的SaaS平台的统一权限管理。我们的SaaS-HRM系统是基于微服务构建,所以在使用Shiro鉴权的时候,就需要将认证信息保存到统一的redis服务器中完成。这样,每个微服务都可以通过指定cookie中的sessionid获取公共的认证信息。

3.2 搭建环境

3.2.1 导入依赖

父工程导入Shiro的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.3.2</version>
</dependency>
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-core</artifactId>
   <version>1.3.2</version>
</dependency>
<dependency>
   <groupId>org.crazycake</groupId>
   <artifactId>shiro-redis</artifactId>
   <version>3.0.0</version>
</dependency>

3.2.2 配置值对象

不需要存入redis太多的用户数据,和获取用户信息的返回对象一致即可,需要实现AuthCachePrincipali接口

1
2
3
4
5
6
7
8
9
10
@Setter
@Getter
public class ProfileResult implements Serializable,AuthCachePrincipal {
   private String mobile;
   private String username;
   private String company;
   private String companyId;
   private Map<String,Object> roles = new HashMap<>();
//省略
}

3.2.3 配置未认证controller

为了在多个微服务中使用,配置公共的未认证未授权的Controller

1
2
3
4
5
6
7
8
9
10
@RestController
@CrossOrigin
public class ErrorController {
   //公共错误跳转
   @RequestMapping(value="autherror")
   public Result autherror(int code) {
       return code ==1?new Result(ResultCode.UNAUTHENTICATED):new
Result(ResultCode.UNAUTHORISE);
  }
}

3.2.4 自定义realm授权

ihrm-common模块下创建公共的认证与授权realm,需要注意的是,此realm只处理授权数据即可,认证方法需要在登录模块中补全。

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
public class IhrmRealm extends AuthorizingRealm {
   @Override
   public void setName(String name) {
       super.setName("ihrmRealm");
  }
   //授权方法
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection
principalCollection) {
       //1.获取安全数据
       ProfileResult result = (ProfileResult)principalCollection.getPrimaryPrincipal();
       //2.获取权限信息
       Set<String> apisPerms = (Set<String>)result.getRoles().get("apis");
       //3.构造权限数据,返回值
       SimpleAuthorizationInfo info = new  SimpleAuthorizationInfo();
       info.setStringPermissions(apisPerms);
       return info;
  }
   /**
    * 认证方法
    */
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken
authenticationToken) throws AuthenticationException {
       return null;
  }
}

3.3.5 自定义会话管理器

之前的程序使用jwt的方式进行用户认证,前端发送后端的是请求头中的token。为了适配之前的程序,在shiro中

需要更改sessionId的获取方式。很好解决,在shiro的会话管理中,可以轻松的使用请求头中的内容作为sessionid

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
public class IhrmWebSessionManager extends DefaultWebSessionManager {
   private static final String AUTHORIZATION = "Authorization";
   private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
   public IhrmWebSessionManager(){
       super();
  }
   protected Serializable getSessionId(ServletRequest request, ServletResponse
response){
       String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
       if(StringUtils.isEmpty(id)){
           //如果没有携带id参数则按照父类的方式在cookie进行获取
           return super.getSessionId(request, response);
      }else{
           id = id.replace("Bearer ", "");
           //如果请求头中有 authToken 则其值为sessionId
         
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_S
ESSION_ID_SOURCE);
           request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id);
         
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TR
UE);
           return id;
      }
  }
}

3.3 用户认证

3.3.1 配置用户登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//用户名密码登录
   @RequestMapping(value="/login",method = RequestMethod.POST)
   public Result login(@RequestBody Map<String,String> loginMap) {
       String mobile = loginMap.get("mobile");
       String password = loginMap.get("password");
       try {
           //1.构造登录令牌 UsernamePasswordToken
           //加密密码
           password = new Md5Hash(password,mobile,3).toString();  //1.密码,盐,加密次数
           UsernamePasswordToken upToken = new UsernamePasswordToken(mobile,password);
           //2.获取subject
           Subject subject = SecurityUtils.getSubject();
           //3.调用login方法,进入realm完成认证
           subject.login(upToken);
           //4.获取sessionId
           String sessionId = (String)subject.getSession().getId();
           //5.构造返回结果
           return new Result(ResultCode.SUCCESS,sessionId);
      }catch (Exception e) {
           return new Result(ResultCode.MOBILEORPASSWORDERROR);
      }
  }

3.3.2 shiro认证

配置用户登录认证的realm域,只需要继承公共的IhrmRealm补充其中的认证方法即可

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
public class UserIhrmRealm extends IhrmRealm {
   @Override
   public void setName(String name) {
       super.setName("customRealm");
  }
   @Autowired
   private UserService userService;
   
   //认证方法
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken
authenticationToken) throws AuthenticationException {
       //1.获取用户的手机号和密码
       UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
       String mobile = upToken.getUsername();
       String password = new String( upToken.getPassword());
       //2.根据手机号查询用户
       User user = userService.findByMobile(mobile);
       //3.判断用户是否存在,用户密码是否和输入密码一致
       if(user != null && user.getPassword().equals(password)) {
           //4.构造安全数据并返回(安全数据:用户基本数据,权限信息 profileResult)
           ProfileResult result = null;
           if("user".equals(user.getLevel())) {
               result = new ProfileResult(user);
          }else {
               Map map = new HashMap();
               if("coAdmin".equals(user.getLevel())) {
                   map.put("enVisible","1");
              }
               List<Permission> list = permissionService.findAll(map);
               result = new ProfileResult(user,list);
          }
           //构造方法:安全数据,密码,realm域名
           SimpleAuthenticationInfo info = new
SimpleAuthenticationInfo(result,user.getPassword(),this.getName());
           return info;
      }
       //返回null,会抛出异常,标识用户名和密码不匹配
return null;
  }
}

3.3.3 获取session数据

baseController中使用shiro从redis中获取认证数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//使用shiro获取
   @ModelAttribute
   public void setResAnReq(HttpServletRequest request,HttpServletResponse response) {
       this.request = request;
       this.response = response;
       //获取session中的安全数据
       Subject subject = SecurityUtils.getSubject();
       //1.subject获取所有的安全数据集合
       PrincipalCollection principals = subject.getPrincipals();
       if(principals != null && !principals.isEmpty()){
           //2.获取安全数据
           ProfileResult result = (ProfileResult)principals.getPrimaryPrincipal();
           this.companyId = result.getCompanyId();
           this.companyName = result.getCompany();
      }
  }

3.4 用户授权

在需要使用的接口上配置@RequiresPermissions(“API-USER-DELETE”)

3.5 配置

构造shiro的配置类

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
@Configuration
public class ShiroConfiguration {
   @Value("${spring.redis.host}")
   private String host;
   @Value("${spring.redis.port}")
   private int port;
   //配置自定义的Realm
   @Bean
   public IhrmRealm getRealm() {
       return new UserIhrmRealm();
  }
   //配置安全管理器
   @Bean
   public SecurityManager securityManager() {
       //使用默认的安全管理器
       DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
       // 自定义session管理 使用redis
       securityManager.setSessionManager(sessionManager());
       // 自定义缓存实现 使用redis
       securityManager.setCacheManager(cacheManager());
       //将自定义的realm交给安全管理器统一调度管理
       securityManager.setRealm(getRealm());
       return securityManager;
  }
   //Filter工厂,设置对应的过滤条件和跳转条件
   @Bean
   public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
       //1.创建shiro过滤器工厂
       ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
       //2.设置安全管理器
       filterFactory.setSecurityManager(securityManager);
       //3.通用配置(配置登录页面,登录成功页面,验证未成功页面)
       filterFactory.setLoginUrl("/autherror?code=1"); //设置登录页面
       filterFactory.setUnauthorizedUrl("/autherror?code=2"); //授权失败跳转页面
       //4.配置过滤器集合
       /**
        * key :访问连接
        *     支持通配符的形式
        * value:过滤器类型
        *     shiro常用过滤器
        *         anno   :匿名访问(表明此链接所有人可以访问)
        *         authc   :认证后访问(表明此链接需登录认证成功之后可以访问)
        */
       Map<String,String> filterMap = new LinkedHashMap<String,String>();
       //配置请求连接过滤器配置
       //匿名访问(所有人员可以使用)
       filterMap.put("/frame/login", "anon");
       filterMap.put("/autherror", "anon");
       //认证之后访问(登录之后可以访问)
       filterMap.put("/**", "authc");
       //5.设置过滤器
       filterFactory.setFilterChainDefinitionMap(filterMap);
       return filterFactory;
  }
   //配置shiro注解支持
   @Bean
   public AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
       AuthorizationAttributeSourceAdvisor advisor = new
AuthorizationAttributeSourceAdvisor();
       advisor.setSecurityManager(securityManager);
       return advisor;
        }
   //配置shiro redisManager
   public RedisManager redisManager() {
       RedisManager redisManager = new RedisManager();
       redisManager.setHost(host);
       redisManager.setPort(port);
       return redisManager;
  }
   //cacheManager缓存 redis实现
   public RedisCacheManager cacheManager() {
       RedisCacheManager redisCacheManager = new RedisCacheManager();
       redisCacheManager.setRedisManager(redisManager());
       return redisCacheManager;
  }
   /**
    * RedisSessionDAO shiro sessionDao层的实现 通过redis
    * 使用的是shiro-redis开源插件
    */
   public RedisSessionDAO redisSessionDAO() {
       RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
       redisSessionDAO.setRedisManager(redisManager());
       return redisSessionDAO;
  }
   /**
    * shiro session的管理
    */
   public DefaultWebSessionManager sessionManager() {
       IhrmWebSessionManager sessionManager = new IhrmWebSessionManager();
       sessionManager.setSessionDAO(redisSessionDAO());
       return sessionManager;
  }
}

第 7 章 POI报表的入门

  • 理解员工管理的的业务逻辑
  • 能够说出Eureka和Feign的作用
  • 理解报表的两种形式和POI的基本操作
  • 熟练使用POI完成Excel的导入导出操作

1 员工管理

1.1 需求分析

企业员工管理是人事资源管理系统中最重要的一个环节,分为对员工入职,转正,离职,调岗,员工报表导入导出等业务逻辑。需求看似复杂,实际上都是对数据库表的基本操作。

1.2 数据库表概述

对于员工操作而言,涉及到的数据库表如下表格说明:

image-20220922103915366

1.3 代码实现

由于此部分内容全部围绕的基本CRUD操作,为了节省课程时间,员工管理的代码以资料的形式给各位学员下发,

学员们直接导入到工程即可。重点功能突出讲解即可

1.3.1 服务端实现

( 1 ) 创建员工微服务ihrm_employee

( 2 ) 配置文件 application.yml

( 3 ) 配置Shiro核心配置类ShiroConfiguration

( 4 ) 配置启动类EmployeeApplication

( 5 )导入资源中提供的基本Controller,Service,Dao,Domain代码

image-20220922103923151

1.3.2 前端实现

导入资源中提供的前端代码。

1.4 服务发现组件 Eureka

Eureka是Netflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflix中,实现SpringCloud的服务发现功能。Eureka包含两个组件:Eureka Server和Eureka Client。

  • Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
  • Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就别一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳,默认周期为 30 秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认 90 秒)。

1.4.1 Eureka服务端开发

( 1 )创建ihrm_eureka模块

( 2 )引入依赖 父工程pom.xml定义SpringCloud版本

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.M9</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

ihrm_eureka模块pom.xml引入eureka-server

1
2
3
4
5
6
<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eurekaserver</artifactId>
        </dependency>
    </dependencies>

( 3 )添加application.yml

1
2
3
4
5
6
7
8
9
server:
port: 6868 #服务端口
eureka:
client:
registerWithEureka: false #是否将自己注册到Eureka服务中,本身就是所有无需
注册
fetchRegistry: false #是否从Eureka中获取注册信息
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址
defaultZone: http://127.0.0.1:${server.port}/eureka/

( 4 ) 配置启动类

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

1.4.2 微服务注册

我们现在就将所有的微服务都注册到Eureka中,这样所有的微服务之间都可以互相调用了。

( 1 )将其他微服务模块添加依赖

1
2
3
4
<dependency>        
<groupId>org.springframework.cloud</groupId>            
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>         
</dependency>

( 2 )修改每个微服务的application.yml,添加注册eureka服务的配置

1
2
3
4
5
6
eureka:
client:
service-url:
defaultZone: http://localhost:6868/eureka
instance:
prefer-ip-address: true

( 3 )修改每个服务类的启动类,添加注解

1
@EnableEurekaClient

1.5 微服务调用组件Feign

1.5.1 简介

Feign是简化Java HTTP客户端开发的工具(java-to-httpclient-binder),它的灵感来自于Retrofit、JAXRS-2.0和

WebSocket。Feign的初衷是降低统一绑定Denominator到HTTP API的复杂度,不区分是否为restful

1.5.2 快速体验

我们现在在系统微服务调用企业微服务的方法(根据ID查询部门)

( 1 )在ihrm_system模块添加依赖

1
2
3
4
<dependency>        
<groupId>org.springframework.cloud</groupId>            
<artifactId>spring-cloud-starter-openfeign</artifactId>            
</dependency>

( 2 )修改ihrm_system模块的启动类,添加注解

1
2
@EnableDiscoveryClient
@EnableFeignClients

( 3 )在Ihrm_system模块创建com.ihrm.system.client包,包下创建接口

1
2
3
4
5
6
//@FeignClient注解用于指定从哪个服务中调用功能 ,注意里面的名称与被调用的服务名保持一致
@FeignClient(value = "ihrm-company")
public interface DepartmentFeignClient {
   //@RequestMapping注解用于对被调用的微服务进行地址映射
   @RequestMapping(value = "/company/departments/{id}/", method = RequestMethod.GET)
   public Department findById(@PathVariable("id") String id) throws Exception; }

( 4 )修改Ihrm_system模块的 UserController

1
2
3
4
5
6
7
8
9
@Autowired    
private DepartmentFeignClient departmentFeignClient;    

//测试通过系统微服务调用企业微服务方法
@RequestMapping(value = "/test/{id}")    
   public void findDeptById(@PathVariable  String id){    
  Department dept = departmentFeignClient.findById(id);  
       System.out.println(dept);        
}

( 5 )配置Feign拦截器添加请求头

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
/**
* FeignConfiguration 过滤器,配置请求头信息
*/
@Configuration
public class FeignConfiguration {
   @Bean
   public RequestInterceptor requestInterceptor() {
       return new RequestInterceptor() {
           @Override
           public void apply(RequestTemplate template) {
               ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder
                      .getRequestAttributes();
               if (attributes != null) {
                   HttpServletRequest request = attributes.getRequest();
                   Enumeration<String> headerNames = request.getHeaderNames();
                   if (headerNames != null) {
                       while (headerNames.hasMoreElements()) {
                           String name = headerNames.nextElement();
                           String values = request.getHeader(name);
                           template.header(name, values);
                      }
                  }
              }
          }
      };
  }
}

2 POI报表的概述

2.1 需求说明

在企业级应用开发中,Excel报表是一种最常见的报表需求。Excel报表开发一般分为两种形式:

  • 为了方便操作,基于Excel的报表批量上传数据
  • 通过java代码生成Excel报表。

在Saas-HRM系统中,也有大量的报表操作,那么接下来的课程就是一起来学习企业级的报表开发。

2.2 Excel的两种形式

目前世面上的Excel分为两个大的版本Excel2003和Excel2007及以上两个版本,两者之间的区别如下:

image-20220922104207973

Excel2003是一个特有的二进制格式,其核心结构是复合文档类型的结构,存储数据量较小;Excel2007 的核心结构是 XML 类型的结构,采用的是基于 XML 的压缩方式,使其占用的空间更小,操作效率更高

2.3 常见excel操作工具

Java中常见的用来操作Excl的方式一般有 2 种:JXL和POI。

  • JXL只能对Excel进行操作,属于比较老的框架,它只支持到Excel 95-2000的版本。现在已经停止更新和维护。
  • POI是apache的项目,可对微软的Word,Excel,Ppt进行操作,包括office2003和2007,Excl2003和 2007 。poi现在一直有更新。所以现在主流使用POI。

2.4 POI的概述

Apache POI是Apache软件基金会的开源项目,由Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java语言操作Microsoft Office的功能。

2.5 POI的应用场景

  1. 数据报表生成

  2. 数据备份

  3. 数据批量上传

3 POI的入门操作

3.1 搭建环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
   <dependency>
       <groupId>org.apache.poi</groupId>
       <artifactId>poi</artifactId>
       <version>4.0.1</version>
   </dependency>
   <dependency>
       <groupId>org.apache.poi</groupId>
       <artifactId>poi-ooxml</artifactId>
       <version>4.0.1</version>
   </dependency>
   <dependency>
       <groupId>org.apache.poi</groupId>
       <artifactId>poi-ooxml-schemas</artifactId>
       <version>4.0.1</version>
   </dependency>
</dependencies>

3.2 POI结构说明

HSSF提供读写Microsoft Excel XLS格式档案的功能。

XSSF提供读写Microsoft Excel OOXML XLSX格式档案的功能。

HWPF提供读写Microsoft Word DOC格式档案的功能。

HSLF提供读写Microsoft PowerPoint格式档案的功能。

HDGF提供读Microsoft Visio格式档案的功能。

HPBF提供读Microsoft Publisher格式档案的功能。

HSMF提供读Microsoft Outlook格式档案的功能。

3.3 API介绍

image-20220922104255836

3.4 基本操作

3.4.1 创建Excel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PoiTest01 {
   //测试创建excel文件
   public static void main(String[] args) throws Exception {
       //1.创建workbook工作簿
       Workbook wb = new XSSFWorkbook();
       //2.创建表单Sheet
       Sheet sheet = wb.createSheet("test");
       //3.文件流
       FileOutputStream fos = new FileOutputStream("E:\\test.xlsx");
       //4.写入文件
       wb.write(fos);
       fos.close();
  }
}

3.4.2 创建单元格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//测试创建单元格
   public static void main(String[] args) throws Exception {
       //1.创建workbook工作簿
       Workbook wb = new XSSFWorkbook();
       //2.创建表单Sheet
       Sheet sheet = wb.createSheet("test");
       //3.创建行对象,从0开始
       Row row = sheet.createRow(3);
       //4.创建单元格,从0开始
       Cell cell = row.createCell(0);
       //5.单元格写入数据
       cell.setCellValue("传智播客");
       //6.文件流
       FileOutputStream fos = new FileOutputStream("E:\\test.xlsx");
       //7.写入文件
       wb.write(fos);
       fos.close();
  }

3.4.3 设置格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//创建单元格样式对象
       CellStyle cellStyle = wb.createCellStyle();
       //设置边框
       cellStyle.setBorderBottom(BorderStyle.DASH_DOT);//下边框
       cellStyle.setBorderTop(BorderStyle.HAIR);//上边框
       //设置字体
       Font font = wb.createFont();//创建字体对象
       font.setFontName("华文行楷");//设置字体
       font.setFontHeightInPoints((short)28);//设置字号
       cellStyle.setFont(font);
       //设置宽高
       sheet.setColumnWidth(0, 31 * 256);//设置第一列的宽度是31个字符宽度
       row.setHeightInPoints(50);//设置行的高度是50个点
       //设置居中显示
       cellStyle.setAlignment(HorizontalAlignment.CENTER);//水平居中
       cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);//垂直居中
       //设置单元格样式
       cell.setCellStyle(cellStyle);
       //合并单元格
       CellRangeAddress  region =new CellRangeAddress(0, 3, 0, 2);
       sheet.addMergedRegion(region);

3.4.4 绘制图形

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
//绘制图形
   public static void main(String[] args) throws Exception {
       //1.创建workbook工作簿
       Workbook wb = new XSSFWorkbook();
       //2.创建表单Sheet
       Sheet sheet = wb.createSheet("test");
       //读取图片流
       FileInputStream stream=new FileInputStream("e:\\logo.jpg");
       byte[] bytes= IOUtils.toByteArray(stream);
       //读取图片到二进制数组
       stream.read(bytes);
       //向Excel添加一张图片,并返回该图片在Excel中的图片集合中的下标
       int pictureIdx = wb.addPicture(bytes,Workbook.PICTURE_TYPE_JPEG);
       //绘图工具类
       CreationHelper helper = wb.getCreationHelper();
       //创建一个绘图对象
       Drawing<?> patriarch = sheet.createDrawingPatriarch();
       //创建锚点,设置图片坐标
       ClientAnchor anchor = helper.createClientAnchor();
       anchor.setCol1(0);//从0开始
       anchor.setRow1(0);//从0开始
       //创建图片
       Picture picture = patriarch.createPicture(anchor, pictureIdx);
       picture.resize();
       //6.文件流
       FileOutputStream fos = new FileOutputStream("E:\\test.xlsx");
       //7.写入文件
       wb.write(fos);
       fos.close();
  }

3.4.5 加载Excel

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
public class PoiTest06 {
   //单元格样式
   public static void main(String[] args) throws Exception {
       //1.创建workbook工作簿
       Workbook wb = new XSSFWorkbook("E:\\demo.xlsx");
       //2.获取sheet 从0开始
       Sheet sheet = wb.getSheetAt(0);
       int totalRowNum = sheet.getLastRowNum();
       Row row = null;
       Cell cell = null;
       //循环所有行
       for (int rowNum = 3; rowNum <sheet.getLastRowNum(); rowNum++) {
           row = sheet.getRow(rowNum);
           StringBuilder sb = new StringBuilder();
           //循环每行中的所有单元格
           for(int cellNum = 2; cellNum < row.getLastCellNum();cellNum++) {
               cell = row.getCell(cellNum);
               sb.append(getValue(cell)).append("-");
          }
           System.out.println(sb.toString());
      }
  }
   //获取数据
   private static Object getValue(Cell cell) {
       Object value = null;
       switch (cell.getCellType()) {
           case STRING: //字符串类型
               value = cell.getStringCellValue();
               break;
               case BOOLEAN: //boolean类型
               value = cell.getBooleanCellValue();
               break;
           case NUMERIC: //数字类型(包含日期和普通数字)
               if(DateUtil.isCellDateFormatted(cell)) {
                   value = cell.getDateCellValue();
              }else{
                   value = cell.getNumericCellValue();
              }
               break;
           case FORMULA: //公式类型
               value = cell.getCellFormula();
               break;
           default:
               break;
      }
       return value;
  }
}

4 POI报表导入

4.1 需求分析

实现批量导入员工功能,页面端上传excel表格,服务端解析表格获取数据,批量新增用户

image-20220922104418513

4.2 员工导入

4.2.1 搭建环境

父模块pom文件添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
           <groupId>org.apache.poi</groupId>
           <artifactId>poi</artifactId>
           <version>4.0.1</version>
       </dependency>
       <dependency>
           <groupId>org.apache.poi</groupId>
           <artifactId>poi-ooxml</artifactId>
           <version>4.0.1</version>
       </dependency>
       <dependency>
           <groupId>org.apache.poi</groupId>
           <artifactId>poi-ooxml-schemas</artifactId>
           <version>4.0.1</version>
       </dependency>

4.2.2 实现Excel上传

( 1 )用户实体类配置构造方法

1
2
3
4
5
6
7
8
9
10
11
12
 //objs数据位置和excel上传位置一致。
public User(Object []objs,String companyId,String companyName) {
       //默认手机号excel读取为字符串会存在科学记数法问题,转化处理
       this.mobile = new DecimalFormat("#").format(objs[2]);
       this.username = objs[1].toString();
       this.createTime = new Date();
       this.timeOfEntry = (Date) objs[5];
       this.formOfEmployment = ((Double) objs[4]).intValue() ;
       this.workNumber = new DecimalFormat("#").format(objs[3]).toString();
       this.companyId = companyId;
       this.companyName = companyName;
  }

( 2 )在系统微服务UserController中添加上传方法

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
//批量导入数据
   @RequestMapping(value="/user/import", method = RequestMethod.POST)
   public Result importExcel(@RequestParam(name = "file") MultipartFile attachment)
throws Exception {
       //根据上传流信息创建工作簿
       Workbook workbook = WorkbookFactory.create(attachment.getInputStream());
       //获取第一个sheet
       Sheet sheet = workbook.getSheetAt(0);
       List<User> users = new ArrayList<>();
       //从第二行开始获取数据
       for (int rowNum = 1; rowNum <sheet.getLastRowNum(); rowNum++) {
           Row row = sheet.getRow(rowNum);
           Object objs[] = new Object[row.getLastCellNum()];
           //从第二列获取数据
           for(int cellNum = 1; cellNum < row.getLastCellNum();cellNum++) {
               Cell cell = row.getCell(cellNum);
               objs[cellNum] = getValue(cell);
          }
           //根据每一列构造用户对象
           User user = new User(objs,companyId,companyName);
           user.setDepartmentId(objs[objs.length-1].toString());
           users.add(user);
      }
       //第一个参数:用户列表,第二个参数:部门编码
       userService.save(users,objs[objs.length-1].toString());
       return Result.SUCCESS();
  }

4.2.3 调用企业微服务获取部门数据

( 1 )在Ihrm_system模块创建com.ihrm.system.client包,包下创建接口

1
2
3
4
5
6
7
8
//远程调用企业微服务,根据企业编码code和企业名称获取企业信息
@FeignClient(value = "ihrm-company")
public interface DepartmentFeignClient {
   @RequestMapping(value = "/company/departments/search/", method =
RequestMethod.POST)
   public Department findById(@RequestParam(value = "code") String code,
                              @RequestParam(value = "companyId") String companyId)
throws Exception; }

( 2 )修改UserService,注入DepartmentFeignClient

1
2
@Autowired
private DepartmentFeignClient departmentFeignClient;

4.2.4 保存全部用户

UserService中添加保存全部的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional
   public void save(List<User> users) throws Exception {
       for (User user : users) {
           //配置密码
           user.setPassword(new Md5Hash("123456",user.getMobile(),3).toString());
           //配置id
           user.setId(idWorker.nextId()+"");
           //其他基本属性
           user.setInServiceStatus(1);
           user.setEnableState(1);
           user.setLevel("user");
           //获取部门信息
           Department dept =
departmentFeignClient.findById(user.getDepartmentId(),user.getCompanyId());
           if(dept != null) {
               user.setDepartmentId(dept.getId());
               user.setDepartmentName(dept.getName());
          }
           userDao.save(user);
           }
  }

5 POI报表导出

5.1 需求分析

完成当月人事报表的导出:包含当月入职员工信息,离职员工信息

image-20220922104550987

5.2 人事报表导出

5.2.1 步骤分析

  • 构造Excel表格数据
  • 创建工作簿
  • 创建sheet
  • 创建行对象
  • 创建单元格对象
  • 填充数据,设置样式
  • 下载

5.2.2 代码实现

( 1 )配置controller

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
@RequestMapping(value = "/export/{month}", method = RequestMethod.GET)
   public void export(@PathVariable(name = "month") String month) throws Exception {
       //1.构造数据
       List<EmployeeReportResult> list =
userCompanyPersonalService.findByReport(companyId,month+"%");
       //2.创建工作簿
       XSSFWorkbook workbook = new XSSFWorkbook();
       //3.构造sheet
       String[] titles = {"编号", "姓名", "手机","最高学历", "国家地区", "护照号", "籍贯",
"生日", "属相","入职时间","离职类型","离职原因","离职时间"};
       Sheet sheet = workbook.createSheet();
        Row row = sheet.createRow(0);
       AtomicInteger headersAi = new AtomicInteger();
       for (String title : titles) {
           Cell cell = row.createCell(headersAi.getAndIncrement());
           cell.setCellValue(title);
      }
       AtomicInteger datasAi = new AtomicInteger(1);
       Cell cell = null;
       for (EmployeeReportResult report : list) {
           Row dataRow = sheet.createRow(datasAi.getAndIncrement());
           //编号
           cell = dataRow.createCell(0);
           cell.setCellValue(report.getUserId());
           //姓名
           cell = dataRow.createCell(1);
           cell.setCellValue(report.getUsername());
           //手机
           cell = dataRow.createCell(2);
           cell.setCellValue(report.getMobile());
           //最高学历
           cell = dataRow.createCell(3);
           cell.setCellValue(report.getTheHighestDegreeOfEducation());
           //国家地区
           cell = dataRow.createCell(4);
           cell.setCellValue(report.getNationalArea());
           //护照号
           cell = dataRow.createCell(5);
           cell.setCellValue(report.getPassportNo());
           //籍贯
           cell = dataRow.createCell(6);
           cell.setCellValue(report.getNativePlace());
           //生日
           cell = dataRow.createCell(7);
           cell.setCellValue(report.getBirthday());
           //属相
           cell = dataRow.createCell(8);
           cell.setCellValue(report.getZodiac());
           //入职时间
           cell = dataRow.createCell(9);
           cell.setCellValue(report.getTimeOfEntry());
           //离职类型
           cell = dataRow.createCell(10);
           cell.setCellValue(report.getTypeOfTurnover());
           //离职原因
           cell = dataRow.createCell(11);
           cell.setCellValue(report.getReasonsForLeaving());
           //离职时间
           cell = dataRow.createCell(12);
            cell.setCellValue(report.getResignationTime());
      }
       String fileName = URLEncoder.encode(month+"人员信息.xlsx", "UTF-8");
       response.setContentType("application/octet-stream");
       response.setHeader("content-disposition", "attachment;filename=" + new
String(fileName.getBytes("ISO8859-1")));
       response.setHeader("filename", fileName);
       workbook.write(response.getOutputStream());
  }

( 2 )添加service

1
2
3
4
//根据企业id和年月查询
   public List<EmployeeReportResult> findByReport(String companyId, String month) {
       return userCompanyPersonalDao.findByReport(companyId,month);
  }

( 3 )dao层实现

1
2
3
4
5
6
@Query(value = "select new 
com.ihrm.domain.employee.response.EmployeeReportResult(a,b) " +
           "FROM UserCompanyPersonal a LEFT JOIN EmployeeResignation b ON
a.userId=b.userId WHERE a.companyId = ?1 AND a.timeOfEntry LIKE ?2 OR
(b.resignationTime LIKE ?2)")
   List<EmployeeReportResult> findByReport(String companyId, String month);

第 8 章 POI报表的高级应用

  • 掌握基于模板打印的POI报表导出
  • 理解自定义工具类的执行流程
  • 熟练使用SXSSFWorkbook完成百万数据报表打印
  • 理解基于事件驱动的POI报表导入

1 模板打印

1.1 概述

自定义生成Excel报表文件还是有很多不尽如意的地方,特别是针对复杂报表头,单元格样式,字体等操作。手写

这些代码不仅费时费力,有时候效果还不太理想。那怎么样才能更方便的对报表样式,报表头进行处理呢?答案是

使用已经准备好的Excel模板,只需要关注模板中的数据即可。

1.2 模板打印的操作步骤

1 .制作模版文件(模版文件的路径)

2.导入(加载)模版文件,从而得到一个工作簿

3.读取工作表

4.读取行

5.读取单元格

6.读取单元格样式

7.设置单元格内容

8.其他单元格就可以使用读到的样式了

1.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
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
@RequestMapping(value = "/export/{month}", method = RequestMethod.GET)
   public void export(@PathVariable(name = "month") String month) throws Exception {
//1.构造数据
List<EmployeeReportResult> list =
userCompanyPersonalService.findByReport(companyId,month+"%");
//2.加载模板流数据
Resource resource = new ClassPathResource("excel-template/hr-demo.xlsx");
FileInputStream fis = new FileInputStream(resource.getFile());
//3.根据文件流,加载指定的工作簿
XSSFWorkbook wb = new XSSFWorkbook(fis);
//4.读取工作表
Sheet sheet = wb.getSheetAt(0);
//5.抽取公共的样式
Row styleRow = sheet.getRow(2);
 CellStyle [] styles = new CellStyle[styleRow.getLastCellNum()];
       for(int i=0;i<styleRow.getLastCellNum();i++) {
           styles[i] = styleRow.getCell(i).getCellStyle();
      }
       //6.构造每行和单元格数据
       AtomicInteger datasAi = new AtomicInteger(2);
       Cell cell = null;
       for (EmployeeReportResult report : list) {
           Row dataRow = sheet.createRow(datasAi.getAndIncrement());
           //编号
           cell = dataRow.createCell(0);
           cell.setCellValue(report.getUserId());
           cell.setCellStyle(styles[0]);
           //姓名
           cell = dataRow.createCell(1);
           cell.setCellValue(report.getUsername());
           cell.setCellStyle(styles[1]);
           //手机
           cell = dataRow.createCell(2);
           cell.setCellValue(report.getMobile());
           cell.setCellStyle(styles[2]);
           //最高学历
           cell = dataRow.createCell(3);
           cell.setCellValue(report.getTheHighestDegreeOfEducation());
           cell.setCellStyle(styles[3]);
           //国家地区
           cell = dataRow.createCell(4);
           cell.setCellValue(report.getNationalArea());
           cell.setCellStyle(styles[4]);
           //护照号
           cell = dataRow.createCell(5);
           cell.setCellValue(report.getPassportNo());
           cell.setCellStyle(styles[5]);
           //籍贯
           cell = dataRow.createCell(6);
           cell.setCellValue(report.getNativePlace());
           cell.setCellStyle(styles[6]);
           //生日
           cell = dataRow.createCell(7);
           cell.setCellValue(report.getBirthday());
           cell.setCellStyle(styles[7]);
           //属相
           cell = dataRow.createCell(8);
           cell.setCellValue(report.getZodiac());
           cell.setCellStyle(styles[8]);
           //入职时间
           cell = dataRow.createCell(9);
           cell.setCellValue(report.getTimeOfEntry());
           cell.setCellStyle(styles[9]);
           //离职类型
           cell = dataRow.createCell(10);
            cell.setCellValue(report.getTypeOfTurnover());
           cell.setCellStyle(styles[10]);
           //离职原因
           cell = dataRow.createCell(11);
           cell.setCellValue(report.getReasonsForLeaving());
           cell.setCellStyle(styles[11]);
           //离职时间
           cell = dataRow.createCell(12);
           cell.setCellStyle(styles[12]);
           cell.setCellValue(report.getResignationTime());
      }
       String fileName = URLEncoder.encode(month+"人员信息.xlsx", "UTF-8");
       response.setContentType("application/octet-stream");
       response.setHeader("content-disposition", "attachment;filename=" + new
String(fileName.getBytes("ISO8859-1")));
       response.setHeader("filename", fileName);
       wb.write(response.getOutputStream());
  }

2 自定义工具类

2.1 自定义注解

( 1 )自定义注解

1
2
3
4
5
6
7
8
9
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelAttribute {
   /** 对应的列名称 */
   String name() default "";
   /** 列序号 */
   int sort();
   /** 字段类型对应的格式 */
   String format() default ""; }

( 2 )导出工具类

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
@Getter
@Setter
public class ExcelExportUtil<T> {
   private int rowIndex;
   private int styleIndex;
   private String templatePath;
   private Class clazz;
   private  Field fields[];
   public ExcelExportUtil(Class clazz,int rowIndex,int styleIndex) {
       this.clazz = clazz;
       this.rowIndex = rowIndex;
       this.styleIndex = styleIndex;
       fields = clazz.getDeclaredFields();
  }
   /**
    * 基于注解导出
    */
   public void export(HttpServletResponse response,InputStream is, List<T> objs,String
fileName) throws Exception {
       XSSFWorkbook workbook = new XSSFWorkbook(is);
       Sheet sheet = workbook.getSheetAt(0);
       CellStyle[] styles = getTemplateStyles(sheet.getRow(styleIndex));
       AtomicInteger datasAi = new AtomicInteger(rowIndex);
       for (T t : objs) {
           Row row = sheet.createRow(datasAi.getAndIncrement());
           for(int i=0;i<styles.length;i++) {
               Cell cell = row.createCell(i);
               cell.setCellStyle(styles[i]);
               for (Field field : fields) {
                   if(field.isAnnotationPresent(ExcelAttribute.class)){
                       field.setAccessible(true);
                       ExcelAttribute ea = field.getAnnotation(ExcelAttribute.class);
                       if(i == ea.sort()) {
                           cell.setCellValue(field.get(t).toString());
                      }
                  }
              }
          }
      }
       fileName = URLEncoder.encode(fileName, "UTF-8");
       response.setContentType("application/octet-stream");
       response.setHeader("content-disposition", "attachment;filename=" + new
String(fileName.getBytes("ISO8859-1")));
       response.setHeader("filename", fileName);
       workbook.write(response.getOutputStream());
  }
   public CellStyle[] getTemplateStyles(Row row) {
       CellStyle [] styles = new CellStyle[row.getLastCellNum()];
       for(int i=0;i<row.getLastCellNum();i++) {
           styles[i] = row.getCell(i).getCellStyle();
      }
       return styles;
  }
}

( 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
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
public class ExcelImportUtil<T> {
   private Class clazz;
   private  Field fields[];
   public ExcelImportUtil(Class clazz) {
       this.clazz = clazz;
       fields = clazz.getDeclaredFields();
  }
   /**
    * 基于注解读取excel
    */
   public List<T> readExcel(InputStream is, int rowIndex,int cellIndex) {
       List<T> list = new ArrayList<T>();
       T entity = null;
       try {
           XSSFWorkbook workbook = new XSSFWorkbook(is);
           Sheet sheet = workbook.getSheetAt(0);
           // 不准确
           int rowLength = sheet.getLastRowNum();
           System.out.println(sheet.getLastRowNum());
           for (int rowNum = rowIndex; rowNum <= sheet.getLastRowNum(); rowNum++) {
               Row row = sheet.getRow(rowNum);
               entity = (T) clazz.newInstance();
               System.out.println(row.getLastCellNum());
               for (int j = cellIndex; j < row.getLastCellNum(); j++) {
                   Cell cell = row.getCell(j);
                   for (Field field : fields) {
                       if(field.isAnnotationPresent(ExcelAttribute.class)){
                           field.setAccessible(true);
                           ExcelAttribute ea =
field.getAnnotation(ExcelAttribute.class);
                           if(j == ea.sort()) {
                               field.set(entity, covertAttrType(field, cell));
                          }
                      }
                  }
              }
               list.add(entity);
          }
      } catch (Exception e) {
           e.printStackTrace();
      }
       return list;
  }
   /**
    * 类型转换 将cell 单元格格式转为 字段类型
    */
   private Object covertAttrType(Field field, Cell cell) throws Exception {
       String fieldType = field.getType().getSimpleName();
       if ("String".equals(fieldType)) {
           return getValue(cell);
      }else if ("Date".equals(fieldType)) {
           return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(getValue(cell)) ;
      }else if ("int".equals(fieldType) || "Integer".equals(fieldType)) {
           return Integer.parseInt(getValue(cell));
      }else if ("double".equals(fieldType) || "Double".equals(fieldType)) {
           return Double.parseDouble(getValue(cell));
      }else {
           return null;
      }
  }
   /**
    * 格式转为String
    * @param cell
    * @return
    */
   public String getValue(Cell cell) {
       if (cell == null) {
           return "";
      }
       switch (cell.getCellType()) {
           case STRING:
               return cell.getRichStringCellValue().getString().trim();
           case NUMERIC:
               if (DateUtil.isCellDateFormatted(cell)) {
                   Date dt = DateUtil.getJavaDate(cell.getNumericCellValue());
                   return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(dt);
              } else {
                   // 防止数值变成科学计数法
                   String strCell = "";
                   Double num = cell.getNumericCellValue();
                   BigDecimal bd = new BigDecimal(num.toString());
                   if (bd != null) {
                       strCell = bd.toPlainString();
                  }
                   // 去除 浮点型 自动加的 .0
                   if (strCell.endsWith(".0")) {
                       strCell = strCell.substring(0, strCell.indexOf("."));
                  }
                   return strCell;
              }
           case BOOLEAN:
               return String.valueOf(cell.getBooleanCellValue());
           default:
               return "";
      }
  }
}

2.2 工具类完成导入导出

( 1 )导入数据

1
List<User> list = new ExcelImportUtil(User.class).readExcel(is, 1, 2);

( 2 )导出数据

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value = "/export/{month}", method = RequestMethod.GET)
   public void export(@PathVariable(name = "month") String month) throws Exception {
       //1.构造数据
       List<EmployeeReportResult> list =
userCompanyPersonalService.findByReport(companyId,month+"%");
       //2.加载模板流数据
       Resource resource = new ClassPathResource("excel-template/hr-demo.xlsx");
       FileInputStream fis = new FileInputStream(resource.getFile());
       new ExcelExportUtil(EmployeeReportResult.class,2,2).
               export(response,fis,list,"人事报表.xlsx");
  }

3 百万数据报表概述

3.1 概述

我们都知道Excel可以分为早期的Excel2003版本(使用POI的HSSF对象操作)和Excel2007版本(使用POI的XSSF操作),两者对百万数据的支持如下:

  • Excel 2003:在POI中使用HSSF对象时,excel 2003最多只允许存储 65536 条数据,一般用来处理较少的数据量。这时对于百万级别数据,Excel肯定容纳不了。

  • Excel 2007:当POI升级到XSSF对象时,它可以直接支持excel2007以上版本,因为它采用ooxml格式。这时excel可以支持 1048576 条数据,单个sheet表就支持近百万条数据。但实际运行时还可能存在问题,原因是执行POI报表所产生的行对象,单元格对象,字体对象,他们都不会销毁,这就导致OOM的风险。

3.2 JDK性能监控工具介绍

没有性能监控工具一切推论都只能停留在理论阶段,我们可以使用Java的性能监控工具来监视程序的运行情况,包括CUP,垃圾回收,内存的分配和使用情况,这让程序的运行阶段变得更加可控,也可以用来证明我们的推测。这里我们使用JDK提供的性能工具Jvisualvm来监控程序运行。

3.2.1 Jvisualvm概述

VisualVM 是Netbeans的profile子项目,已在JDK6.0 update 7 中自带,能够监控线程,内存情况,查看方法的

CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈

3.2.2 Jvisualvm的位置

Jvisualvm位于JAVA_HOME/bin目录下,直接双击就可以打开该程序。如果只是监控本地的java进程,是不需要配置参数的,直接打开就能够进行监控。首先我们需要在本地打开一个Java程序,例如我打开员工微服务进程,这时在jvisualvm界面就可以看到与IDEA相关的Java进程了:

image-20220922105016284

3.2.3 Jvisualvm的使用

Jvisualvm使用起来比较简单,双击点击当前运行的进程即可进入到程序的监控界面

image-20220922105024196

概述:可以看到进程的启动参数。

监视:左上:cpu利用率,gc状态的监控,右上:堆利用率,永久内存区的利用率,左下:类的监控,右下:线程的监控

线程:能够显示线程的名称和运行的状态,在调试多线程时必不可少,而且可以点进一个线程查看这个线程的详细运行情况

3.3 解决方案分析

对于百万数据量的Excel导入导出,只讨论基于Excel2007的解决方法。在ApachePoi 官方提供了对操作大数据量的

导入导出的工具和解决办法,操作Excel2007使用XSSF对象,可以分为三种模式:

  • 用户模式:用户模式有许多封装好的方法操作简单,但创建太多的对象,非常耗内存(之前使用的方法)
  • 事件模式:基于SAX方式解析XML,SAX全称Simple API for XML,它是一个接口,也是一个软件包。它是一种XML解析的替代方法,不同于DOM解析XML文档时把所有内容一次性加载到内存中的方式,它逐行扫描文档,一边扫描,一边解析
  • SXSSF对象:是用来生成海量excel数据文件,主要原理是借助临时存储空间生成excel

image-20220922105058894

这是一张Apache POI官方提供的图片,描述了基于用户模式,事件模式,以及使用SXSSF三种方式操作Excel的特性以及CUP和内存占用情况。

4 百万数据报表导出

4.1 需求分析

使用Apache POI完成百万数据量的Excel报表导出

4.2 解决方案

4.2.1 思路分析

基于XSSFWork导出Excel报表,是通过将所有单元格对象保存到内存中,当所有的Excel单元格全部创建完成之后一次性写入到Excel并导出。当百万数据级别的Excel导出时,随着表格的不断创建,内存中对象越来越多,直至内存溢出。Apache Poi提供了SXSSFWork对象,专门用于处理大数据量Excel报表导出。

4.2.2 原理分析

在实例化SXSSFWork这个对象时,可以指定在内存中所产生的POI导出相关对象的数量(默认 100 ),一旦内存中的对象的个数达到这个指定值时,就将内存中的这些对象的内容写入到磁盘中(XML的文件格式),就可以将这些对象从内存中销毁,以后只要达到这个值,就会以类似的处理方式处理,直至Excel导出完成。

4.3 代码实现

在原有代码的基础上替换之前的XSSFWorkbook,使用SXSSFWorkbook完成创建过程即可

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
//1.构造数据
       List<EmployeeReportResult> list =
userCompanyPersonalService.findByReport(companyId,month+"%");
       //2.创建工作簿
       SXSSFWorkbook workbook = new SXSSFWorkbook();
       //3.构造sheet
       String[] titles = {"编号", "姓名", "手机","最高学历", "国家地区", "护照号", "籍贯",
"生日", "属相","入职时间","离职类型","离职原因","离职时间"};
       Sheet sheet = workbook.createSheet();
       Row row = sheet.createRow(0);
       AtomicInteger headersAi = new AtomicInteger();
       for (String title : titles) {
           Cell cell = row.createCell(headersAi.getAndIncrement());
           cell.setCellValue(title);
      }
       AtomicInteger datasAi = new AtomicInteger(1);
        Cell cell = null;
       for(int i=0;i<10000;i++) {
           for (EmployeeReportResult report : list) {
               Row dataRow = sheet.createRow(datasAi.getAndIncrement());
               //编号
               cell = dataRow.createCell(0);
               cell.setCellValue(report.getUserId());
               //姓名
               cell = dataRow.createCell(1);
               cell.setCellValue(report.getUsername());
               //手机
               cell = dataRow.createCell(2);
               cell.setCellValue(report.getMobile());
               //最高学历
               cell = dataRow.createCell(3);
               cell.setCellValue(report.getTheHighestDegreeOfEducation());
               //国家地区
               cell = dataRow.createCell(4);
               cell.setCellValue(report.getNationalArea());
               //护照号
               cell = dataRow.createCell(5);
               cell.setCellValue(report.getPassportNo());
               //籍贯
               cell = dataRow.createCell(6);
               cell.setCellValue(report.getNativePlace());
               //生日
               cell = dataRow.createCell(7);
               cell.setCellValue(report.getBirthday());
               //属相
               cell = dataRow.createCell(8);
               cell.setCellValue(report.getZodiac());
               //入职时间
               cell = dataRow.createCell(9);
               cell.setCellValue(report.getTimeOfEntry());
               //离职类型
               cell = dataRow.createCell(10);
               cell.setCellValue(report.getTypeOfTurnover());
               //离职原因
               cell = dataRow.createCell(11);
               cell.setCellValue(report.getReasonsForLeaving());
               //离职时间
               cell = dataRow.createCell(12);
               cell.setCellValue(report.getResignationTime());
          }
      }
       String fileName = URLEncoder.encode(month+"人员信息.xlsx", "UTF-8");
       response.setContentType("application/octet-stream");
       response.setHeader("content-disposition", "attachment;filename=" + new
String(fileName.getBytes("ISO8859-1")));
       response.setHeader("filename", fileName);
       workbook.write(response.getOutputStream());

4.4 对比测试

( 1 )XSSFWorkbook生成百万数据报表

使用XSSFWorkbook生成Excel报表,时间较长,随着时间推移,内存占用原来越多,直至内存溢出

image-20220922105145514

( 2 )SXSSFWorkbook生成百万数据报表

使用SXSSFWorkbook生成Excel报表,内存占用比较平缓

image-20220922105150938

5 百万数据报表读取

5.1 需求分析

使用POI基于事件模式解析案例提供的Excel文件

5.2 解决方案

5.2.1 思路分析

  • 用户模式:加载并读取Excel时,是通过一次性的将所有数据加载到内存中再去解析每个单元格内容。当Excel数据量较大时,由于不同的运行环境可能会造成内存不足甚至OOM异常。

  • 事件模式:它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势。

5.2.2 步骤分析

( 1 )设置POI的事件模式

  • 根据Excel获取文件流
  • 根据文件流创建OPCPackage
  • 创建XSSFReader对象

( 2 )Sax解析

  • 自定义Sheet处理器
  • 创建Sax的XmlReader对象
  • 设置Sheet的事件处理器
  • 逐行读取

5.2.3 原理分析

我们都知道对于Excel2007的实质是一种特殊的XML存储数据,那就可以使用基于SAX的方式解析XML完成Excel的读取。SAX提供了一种从XML文档中读取数据的机制。它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势

image-20220922105225125

5.3 代码实现

5.3.1 自定义处理器

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
//自定义Sheet基于Sax的解析处理器
public class SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
   //封装实体对象
   private PoiEntity entity;
   /**
    * 解析行开始
    */
   @Override
   public void startRow(int rowNum) {
       if (rowNum >0 ) {
           entity = new PoiEntity();
      }
  }
   /**
   * 解析每一个单元格
    */
   @Override
   public void cell(String cellReference, String formattedValue, XSSFComment comment)
{
       if(entity != null) {
           switch (cellReference.substring(0, 1)) {
               case "A":
                   entity.setId(formattedValue);
                   break;
               case "B":
                   entity.setBreast(formattedValue);
                   break;
               case "C":
                   entity.setAdipocytes(formattedValue);
                   break;
               case "D":
                   entity.setNegative(formattedValue);
                   break;
               case "E":
                   entity.setStaining(formattedValue);
                   break;
               case "F":
                   entity.setSupportive(formattedValue);
                   break;
               default:
                   break;
          }
      }
  }
   /**
    * 解析行结束
    */
   public void endRow(int rowNum) {
       System.out.println(entity);
  }
   //处理头尾
   public void headerFooter(String text, boolean isHeader, String tagName) {
  }
}

5.3.2 自定义解析

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
/**
* 自定义Excel解析器
*/
public class ExcelParser {
   public void parse (String path) throws Exception {
       //1.根据Excel获取OPCPackage对象
       OPCPackage pkg = OPCPackage.open(path, PackageAccess.READ);
       try {
           //2.创建XSSFReader对象
           XSSFReader reader = new XSSFReader(pkg);
           //3.获取SharedStringsTable对象
           SharedStringsTable sst = reader.getSharedStringsTable();
           //4.获取StylesTable对象
           StylesTable styles = reader.getStylesTable();
           //5.创建Sax的XmlReader对象
           XMLReader parser = XMLReaderFactory.createXMLReader();
           //6.设置处理器
           parser.setContentHandler(new XSSFSheetXMLHandler(styles,sst, new
SheetHandler(), false));
           XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator)
reader.getSheetsData();
           //7.逐行读取
           while (sheets.hasNext()) {
               InputStream sheetstream = sheets.next();
               InputSource sheetSource = new InputSource(sheetstream);
               try {
                   parser.parse(sheetSource);
              } finally {
                   sheetstream.close();
              }
          }
      } finally {
           pkg.close();
      }
  }
}

5.3.3 对比测试

用户模式下读取测试Excel文件直接内存溢出,测试Excel文件映射到内存中还是占用了不少内存;事件模式下可以流畅的运行。

( 1 )使用用户模型解析

image-20220922105335027

( 2 )使用事件模型解析

image-20220922105340708

5.4 总结

通过简单的分析以及运行两种模式进行比较,可以看到用户模式下使用更简单的代码实现了Excel读取,但是在读取大文件时CPU和内存都不理想;而事件模式虽然代码写起来比较繁琐,但是在读取大文件时CPU和内存更加占优。

第 9 章 文件上传与PDF报表入门

  • 理解DataURL的基本使用,实现DataURL的文件上传
  • 完成基于七牛云的文件上传
  • 理解 JasperReport生命周期
  • 独立完成 JasperReport的入门案例

1 图片上传

1.1 需求分析

image-20220922105410053

如图所示,实现员工照片上传功能

1.2 Data URL

1.2.1 DataURL概述

所谓DataURL是指”data”类型的Url格式,是在RFC2397中提出的,目的是对于一些“小”的数据,可以在网页中直接

嵌入,而不是从外部文件载入。

1.2.2 Data URL入门

  • 完整的DataURL语法:DataURL= data:mediatype;base64,<Base64编码的数据>。

  • mediatype:表述传递的数据的MIME类型(text/html,image/png,image/jpg)

简单的说,data类型的Url大致有下面几种形式。

1
2
3
4
5
6
7
8
9
10
11
12
data:,<文本数据>
data:text/plain,<文本数据>
data:text/html,<html代码>
data:text/html;base64,<base64编码的html代码>
data:text/css,<css代码>
data:text/css;base64,<base64编码的css代码>
data:text/javascript,<javascript代码>
data:text/javascript;base64,<base64编码的javascript代码>
data:image/gif;base64,base64编码的gif图片数据
data:image/png;base64,base64编码的png图片数据
data:image/jpeg;base64,base64编码的jpeg图片数据
data:image/x-icon;base64,base64编码的icon图片数据

对于再程序开发中,使用最多的是基于DataURL的图片形式,接下来以图片形式的DataURL分析其原理和利弊

1.2.3 Data URL基本原理

Data URL给了我们一种很巧妙的将图片“嵌入”到HTML中的方法。跟传统的用img标记将服务器上的图片引用到页面中的方式不一样,在Data URL协议中,图片被转换成base64编码的字符串形式,并存储在URL中,冠以mime-type。

图片在网页中的使用方法通常是下面这种利用img标记的形式:

1
<img src="images/myimage.gif ">

这种方式中,img标记的src属性指定了一个远程服务器上的资源。当网页加载到浏览器中时,浏览器会针对每个外部资源都向服务器发送一次拉取资源请求,占用网络资源。大多数的浏览器都有一个并发请求数不能超过 4 个的限制。这意味着,如果一个网页里嵌入了过多的外部资源,这些请求会导致整个页面的加载延迟。而使用Data URL技术,图片数据以base64字符串格式嵌入到了页面中,与HTML成为一体,它的形式如下:

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
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAA5CAYAAACMNEHAAAAAGXRFWHRTb2Z0d2FyZ
QBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4RpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp
bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU
6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy
0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyL
zIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0
dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3h
hcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC
8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo5ZDAxYjE4NC04MTFlLTE4NDEtYTUwYi0wMzljN
jZmNDA1YWUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MUYyRjEwQUFGMEY3MTFFN0IzQUU5ODg2NUEzREJF
MDMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MUYyRjEwQTlGMEY3MTFFN0IzQUU5ODg2NUEzREJFMDMiIHh
tcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTcgKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZW
RGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ZDA1ZmRhNzAtMzY1Yi1iNTQwLWE3OTYtOGVjNzJkYTQwZ
mMyIiBzdFJlZjpkb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MDM4NzM5MTYtZTQ4ZS0xMWU3LWFl
NTUtODVlYjU5NWU3MzVlIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3h
wYWNrZXQgZW5kPSJyIj8+d+H9YQAAE4NJREFUeNrsXQmYFMUVroWVWxAC4kEETxAhSlQMShBQAooH4hlPxCh4i6
JGkxiNJkbFoKJGjSiKBypyeB/IYcAzEg8uDxQEDERBdpXLZSH1f/tX5lFb3V3d07OLS7/ve9/M9NRUV1f99ep/r
44pqjPoYbUZSUOtHbTurXVPra20/lTrT7Q2oxop17pC6zdaP9E6V+sMrf/UWpK0AF0++rvKpOZKcTXfv77Wblp7
aO2udT+ttT1/i3QtqOgcR/P6eq2vaX1Q69P8nEkm1Qb4+gTnCVp7a21QgGfqTf1c69Van8iaOhNIrSq8175a79P
6X62Paz2mAGC3ZRetY7SO09oka+5MiqugQ/XVeoXWrtX4nOhcu2vtqfXrrNkzC18IAW15T+sz1Qx2I3CGX9FaL2
v2zMKnKT/XOkLrgZvh8+6j9Vqtv03wWzjGl2mdRKfYNVKcrHUP8flOVRFFiiP9qJCVWodo3Vig+tiJ7QVBZGtKN
bZNa61dtG6vdTu+Dta62vP3j6qKKJ+R27ReovVCrYu0NkUgI03Ab6P1Jq1nay3ajDv5pVpHa52dAIhnUSF7aZ1j
pfmrqgijQlZpvSHmPeBn3KO1JT9fV0CwK1K8B/n+AxoE428BII1oFPG6FQFVh75XPZbXaGO+LtB6VIKyNKZvJ+V
Fx7UgOYXlH0CgA4/daISAy/74nBbg+9Ih3SHlBinVOkvrp3R2V/C6qWzE6w9iI/gKGm6Y1sMSPKORzx1gby3ADp
mu4odEbxVghwxiA7rkJFURyu3jke9Yjja+8geVC/PGlTaOawdr7eTxW7TxtuLzEKs+XLKYz+eSGayjmwj+GfkCv
g4b6YKUAL6KvRq0YTKBHiVN2YuvsSorTACSjlo/8kzfnMOtkccdjdjB+k1tWpogeVbrfPH5TDF6GNku5Pfo9O1Y
jih5P2Y7zIoJ+DKOBLDu36qKicLlVtDg4gR42J8aJtNCAI/JyYVsO1DLJvkAHj35SY8CRckGrS9oHcXXNTF/jwq
+S1WEH5/2BADkHPI7HznBcvAfpfUNa8RDqUGyQAAeeV8tvpvDzr6/GDWXan1bpPlGFU7GiQ5bRov/pdb2gqZ9py
oiX6Wshxu1vkVKu2ozorCYgxmpdSB8t6SA705wNcujIKgwzOPfzV6Yrywn7XjLYW1dckwMwJ9hUZW5KTcKOv3pz
Hs+o1royC8LwL/IRguS260RZUKIlQYI2lrt15Yjwb+0/kbrUI4i9fi5L8sky7yMQDdBgOO1Pqb1uYjnvS7P+hoc
QHVAed/h+x/IEmaw7oCLyUkAj0p8ilw4iSAaMJwNtLKS2bv3kvBhZdBtUZQI9GqqRzl21Lqb1s8i0oE2dBafR/J
1PK204e+y4O+y4cPkI4syDWLkZyHrR1mjZ2eOgmlIW/o/NkXaW7QJjMGuvC8MSC+LgkHqq1zIGSPzcRylo+Ravr
Zgp+4kOtERBGhUAMEF+C9FvSO6cwffX8bXO+IC/qg8wL6ejtOf7eE4CuSutCHAn0ancheP7Dp6AP5CR/7m1by3H
wCd4t4YddNIjCJBTtpeVCkPFYACNBcda4mgX7+kBTfSgCP0VwTqJ6R+JxBs53nU/Xh2KiWo3QuqgBIH8L3oHCQB
+2Ra3rlJQJ4A+G96Ar6V/OBYKdncQSMQJuyhNl2qcKKVpq7KxdLDZDo7/wZVeYVnQ0f7rFLpLIa7hnSmq3CUF/F
6PQLZ9tfaOKJdkurtLUaNEgvwGM0mClDDiNyskk8CSn9mViEAv09CsJfQoXmYQAkCOirqEK2/UBWTIS3YsHiwNz
iqzIxxX1+HrqmHdXc1ynAHJbD5tI/0IP3CULyNuL4rOWdzh6M7gDxbWRTn4hAn2u5Mz4j3Zwn+O4pGwJ6Y216Fh
36/onNrpNQx4o2k7zdcUA0ZeHg9IO8dHIERRMkuLZSFb8kQWuMEVn0ALYfLqteh9RykchMetmDWshsbYCStxg8e
9/ZdMlEa8dxDqiGqcBipSnPBaz9WFTO9e7EjIK78J8/8ylW81aKLhTVvSAp6seDp9cVIVyQAj7YOmszbhc/UNcT
wfEcQmxnsIo4g9hCOibLLC0VptmLosVVMrn6VqojPb3SAHQ9yKivypzHyPYuW4HKb2jhoza6eeX4f8h0AtXXAd8
+oXGy7j8W7x8YIyy0V7zGBdqXWI600uHYPo2K/YrgQIUxM0swTfoRLVjMKNsejLLUJyHLShZPZRjuJ0QXhydHCW
PyNERfsY/iAI/lwVXl+Y6EVoPiWGIHFP4nXTmXk7G7SlCsZBjWyjm0/Ih9rUhSx4+k2FW/CYCkdm+kBVn1nrQ/w
QZPIWkZXVkREbpYov1lf+CWTHPx9azaKazPKziI6U58WqaHgwa1V/OUA6KD/tjrYGlKqkWJEvF/raapizuFkld+
yA5T5CvJ2KaAW26lN1wS9z7Akrk0RNAk07Hqtv7fyAPjP5DPJOoVv9aqqiOUvp/HDqH2LGDlcgnuer1IIB4cB/k
iL60UJHvJwDm8usKOB7lObLvBJIv2EA+QC+89YFh/ZVgyhNuiX07HbYFEkCfj+tLxGxhEAYVJGy1xuXQegnrNGn
+V2e9Hi2pw5jJ50dQD9bVIkF/WznxdhvnM4arUT/F5GYk7iKGQc+bd53/UOqrk1772vys2ktougoRhNsPL2Q0aD
vuAI9x++ejvyxSH89YEYIHyFlr3UAfQiDo1XpcRxd4r4vr9nPp+q8LXxGJYP5hB7gWdZ+nveH43XmeAy8ryqmHw
ayBGwETXIkUsqqxzhTZsjdyGdWM+0MwLSvm910CYcmc4QIOxGfr87R4io5R+lHMFlVKgxHfweAb+5U3lOIroAX0
RvvblnBcIqHRvgTBYRMINTdOrCnNZmyn/29PmIkOSHzO92ld5aISP7kjZ8ZV0fzWiVdDgXx8y7WYjvITl0U0ek5
TTS0f0I8uKIaJRLPqGTbeRr0pswAcd/gSP3FLZxO456fRm9C6M8visqnYAfqPxW4ClWzokShJZ1H5Ey2JXFC225
Wfkvd3gq4vsPGd1YH0EZpnk6g7YVWxczYuIrPn7XEtKOVTRWBpiTBV2RYia40El7i5FirBjpgqzvXDKAX1nXl9F
feF106vqkxUbmM4QJo9mRo2JntnE56xYj8RtJAY8HGub52/nk06sDwD6UjkaaAuvxbgB/N+vVfWSuRyWNJijDAI
cGf4l1sMHxfR1VeTa0lI2/3KOcGMrjztB18kyznlTjWI/0A/jaXQD+G3G9XwjgFaNKT9KIvMYRqCVp8PEJsXBAS
GDBG/C3q00nQMJoxYmy0SywYxr6JpW+XGkiExbYW4tohq8VjJJ1nnmNoaM8ivz3CwH2sVaYcQ0B87Zn3k1Vbl1N
mhJ3ttY46XISrpW4HrUZ/z2qCjAMSQQW/vO4Pyq2ogQneP7uOvEANtjx8I+o9PfLIpw0wQH2hrzeLEbjjbIvvtn
x3CSHMPVTuY0hf6BOIfCPd4AdC6PeUj8+aR1A1Vrnme98JULYEYKFfgfl+yDFwrm8xfM3H0ekvcIjkhJX0LnODa
AMWIC0T4y8rlJ+s7U+4qJQrmiCAfvkmPkvUfE3wF/j4STGFbOct43KrZ8pESNlO1V5PZGvDzjAM+2ANAGP6ew9P
X+DSYayAOsOznlZypU9n1Z0tWXdDWXoFbOCndPsCY/YgwU/kJ0RFr1uSBSiCa1ieYz868TszJAWMdL6znSbSu8q
AF8qrvdNCPjOnvQS0j4NMBWHWCqXYCntuJDvMT3cKEWwz2bEaJkF9rqMshwZI6/VjEClvSn6DeoQggHgt5c2tGO
9LeL3z8cA7/gCUJQPOQo38eT13wZ0lm/zLMeeMQxtKlKLHPgIz/QjI5yOw1Ms2wekBostsMOpfjUm2CGIz39awL
pE1ALrh3ZnFOYlRxrs9ZxVhe27MuD6F6rySWxp7eL6roqebUVSC99L+e/6n7CJ91d5qW/nlB4G98EkyPeWg4qZu
om0mHEE0/+BM8cpnxi8kR0S2oGW/1TW8fUq3nZGRMGGxrz/yYLm/S3EmMijNLAG6saQPKNWjXZUuX0DcaJKmNy6
3zOtXLsPDHyZFPDdPNOCVsyLSJPv+Y1r6COg0jZaYMfo8ajyC5tKmaPC94L6WJIhCS3LLDbS70h3hnt29gXCQo+
KWd6lBNIb7HQuwRwDQnprea+ZliO/VG267zSKZ7cjAKeq6Ik4rM1pIHwq3+f7jMEVYPDupI2JxWOTVfikgZGXlH
WWi8PCo4H3SliWMQTG5w7n9C8JnWET5VgQlig7E37LkWLlt8NfeQ7FiD8Pi3F/WPTHOfTOtoCuWDbE3zsleDZw6
kOjwJ7Jlgd43zCWzwzZbQwfDYzIZyqjLGOMY2UBHVGYq6lJTlZYQrDPy5o4ExvwvtKNUZ3/A9+AVFCbcnJW8LJf
k96AknxNC44zQ6bI6IFjPfth5LptEz4TJsb6ZJY9kyAOv1YFT5jYAmQ7NyjHPYXAAXSMDLcq/5WaLoHDhG1iseL
DGYffsgA/L4Y1xeQETml9MinoHUDfjZGZ01V+pw4DtVgWW5aBPZMwwD9EsMWRW8mvf/AFvgPooDvYlIsYde08ng
EzqOerBKdy1SCwt1G5VZpNBWUEPcRG/GeqoUxYFYpNRI9uThVVS0VvhJCCSZXl7CDg4v1dVlmCG+8tsMOZxO4Wh
DDPyBPsiLF33sLBHiTDWM+7VMO9sbgQYezdN0en9QUCp70H2L+kY/oeqQgWTmHGDuu7MaFQEmLRIWZZQBqCGbqL
VPzThmsi2Bc4DM9uIekbsCNgAdgSVXlBGwINWGuD9e/Y+ueabDP/n/s108g1SnGAXsSy4J4LVfA/fhTxmRA0mZ+
PhUcGZ0dwXzi22CGOTRHYUrUPezFm2LALCZNXWDG3axU0LmZ8j2aZ11QRoGazQc3x1135GWrO7BnEz9jO9jrfmx
WEe4r0ZvXjsfwM4zGO783e2W1FenPP7sLouCiNSQ+jIk8OHq5yESu0N5Y3fMN2XMjv5D7agRzFP2UapB2vcps/A
M6ZLAe2Wy5mHuYvjkaJe/9RhUfLMOH5GXUOgw23iM47is80jG3wCdO+paJPjQsEPATT0IMtXmxkAx92ECsAywdw
3MZfWJDx7O2P5dPzPAXLgTvmw0kTWnezQvRwQcuMGLD0FWnHWdckoHry9QiR3qyI7ONI393Ky4eCYsQ1hzzNYie
EXMIAQTmd/BfZYcfRWuP9vRwBRhB8wEI/ldsKiA7Uidz8LJYHo8HDxNNMce+Pxb1twW+eZQdCOe5iuYaqyvM4mG
WfR98RhvkA4jEx4CEP8AHK2KvM+YVr+B2m/bGmBEcidKP16kUO/XqBgY4KxFHMx6vq+dvJCcIJtAHZgwbhENbbe
AvAtawO0pMWzOSFhW3PMQLWU+QV1KEmeJT3RpXbRogVrufw/UUC+OcxT1hRnCnTmBa2NwGOtNeq3A6tVtZrXY76
5xAP+9I43iHuPUbc2xaAGit1p9OQXMDgw32q8tmgU+kvDlW547Db5At4A/pDVe5ckPvI849jVOYQ9toT+eDY0va
QSm8Hkctv+AcpwdPVyJFBOxaRwnWghZnJoR8W+GBaxRmkXAv5PWax92eaLzgc/5JGoiWt1jwCDY1an3kdymvv8v
cdWAdmYVgSAUDNlrwPRP1id9SVLN8qlr8nDV6J6HD1Bc1QKncW/DLiIO4aKrPiVZ5t8yAt90QrrUxjDF69NACva
K1RwY+Qi11Gj3s8K/xjcvpxKr0NuUEg60ILsTKNDPN0Vg1N+StDfa/QdwGILrTSSEt8DWnga/xNY5U7mm6sI/8L
mOdU1ntt3lMRCBvyMB5G5CnQzQQO9iYvv4id9myVW9dvfj+CPsxIpqnDkX6qire1c6OjLA2Ue0O4PP04r7/TCdp
oXUr6AuuyHR+6LdPXUYX9K0UjvZT/7v6qkPEWtXhN8NO+gp7Y6Q3vn0SV/F3Sk4ms1yNE/pMC8m9B53c3j3Kb5d
Q/qNzaot4iGgNL/j2N23EE4ERy9lEqdzTiepVb44SOcANZQHsCciuVW4jo6pQ7ssw78vNHghKa/Ri/Z1kKFruPO
llgkaAt33EIq6o/rEq1U6UQipwuhtN1/DxJfD/TiqCYv9s0z4LRYIoAwwIlTn5gaO8d8Rl5vynq29AeyCm0xGGb
J1aI6NFk4XCaUWcK89iZbTtV5U5C60P+/hQpmKJTu46+xxnEwrVkAE1YzpnWvQer3OGrl/N+lwv6jGfagxGYV0i
tilQBJ8p8j9LYoPz/EbmmSjkpSAkbx2yeeJ/XXMe9PcHv3mRnWUlgldChC0o/j9SxjBGMEoKvTHS4EpU77tv8g0
iJMBSjeT+M0PvRisInu1T4Hu3pJxzG3z5IalWPIUV0hj/xO7NEG87jkwxB/5GBjtnMw0RnHuG9W/LedRn8KBGh5
KX0Vd7lSNWLHeVSldtov5q/WWuFyEuS4jHquOy0ZRvlv7CraVrcPSUL/2OUrVTuP1PLLEO3La2yaw8qAN9Ihf+T
ShEBHZRH0L1d0pjcfVmh6XKxyqQmS5na9E8X5Ii9NOR3ay2rGkQ5lya4d5DPWFoVFVJrM26sjRleM9kSAI//7ey
iKv8RVyaZ1ChK87LadGYvk0xqJOCn0dufljVHJjWZ0gDgmHTonoE9k5ps4TFhg8msqVn1Z1LV8j8BBgDJTf+jYX
O34QAAAABJRU5ErkJggg==

上面的base64字符串中你看不出任何跟图片相关的东西,但下面,我们将传统的img写法和现在的Data URL用法左右对比显示,你就能看出它们是完全一样的效果。但实际上它们是不一样的,它们一个是引用了外部资源,一个是使用了Data URL。

1.2.4 优缺点分析

  1. 浏览器支持

几乎所有的现代浏览器都支持Data URL格式,包括火狐浏览器,谷歌浏览器,Safari浏览器,opera浏览器。IE8也支持,但有部分限制,IE9完全支持。

  1. 数据容量

Base64编码的数据体积是原数据的体积4/3,也就是DataURL形式的图片会比二进制格式的图片体积大1/3。

  1. 使用场景

DataURL形式的数据不会占用HTTP会话,所以再访问外部资源或当图片是在服务器端用程序动态生成时借用

DataURL是一个不错的选择

1.3 Data URL实现用户头像上传

  • 修改用户实体类,用户数据库表添加用户头像字段
  • 使用基于Data URL的方式实现用户上传,实质是将前端上传的文件以Base64进行编码并且保存到数据库中。
  • 用户controller中添加用户上传方法
  • 用户service中添加上传文件处理的方法
  • 在service中需要对文件进行base64编码,并且保存到数据库中

( 1 )在系统微服务的UserController中添加上传处理的方法

1
2
3
4
5
6
 @RequestMapping(value="/user/upload/{id}")
   public Result upload(@PathVariable String id,@RequestParam(name = "file")
MultipartFile file) throws Exception {
       String image = userService.uploadImage(id, file);
       return new Result(ResultCode.SUCCESS,image);
  }

( 2 )在系统微服务的UserService中添加上传处理的方法

1
2
3
4
5
6
7
8
9
10
11
12
 public String uploadImage(String id, MultipartFile file) throws Exception {
       //根据id查询用户
       User user = userDao.findById(id).get();
       //对上传文件进行Base64编码
       String s = Base64.encode(file.getBytes());
       //拼接DataURL数据头
       String dataUrl = new String("data:image/jpg;base64,"+s);
       user.setStaffPhoto(dataUrl);
       //保存图片信息
       userDao.save(user);
       return dataUrl;
  }

2 七牛云存储

1.1 概述

七牛云对象存储服务提供高可靠、强安全、低成本、可扩展的非结构化数据的存储服务。它提供简单的 Web 服务接口,可以通过七牛开发者平台或客户端存储和检索任意数量的数据,支持 “按使用付费” 模式,可以通过调用REST API 接口和 SDK开发工具包访问,下载协议采用HTTP 和 HTTPS 协议。方便程序员聚焦业务应用,而无需关

注底层存储实现技术。

使用七牛云实现图片存储也比较简单只需要按照如下的步骤操作即可:

  1. 申请七牛云账号

  2. 创建空间 Bucket

  3. 上传文件

  4. 请求获取图片

1.2 账户申请

( 1 ) 进入七牛云官方网站注册开发者账户

七牛云是通过邮箱注册的,注册激活后就进行认证,认证后即可开通对象存储业务了

image-20220922110813506

( 2 )创建存储空间 Bucket

点击左侧左侧菜单 对象存储,一开始我们需要新建一个存储空间来存放我们的图片资源。点击新建存储空间,设置一些需要的内容,然后在左侧的存储空间列表我们就可以看到新加的空间了。

image-20220922110821294

账号注册有些需要注意的点如下:

  • 注册账号之后需要实名认证(个人/企业)
  • 实名认证之后才可以创建存储空间
  • 存储空间创建成功之后,找到个人中心获取accessKey,secretKey和存储空间名称就可以进行上传操作了

1.3 入门案例

七牛对象存储将数据文件以资源的形式上传到空间中。可以创建一个或者多个空间,然后向每个空间中上传一个或

多个文件。通过获取已上传文件的地址进行文件的分享和下载

1.3.1 搭建环境

1
2
3
4
5
<dependency>
 <groupId>com.qiniu</groupId>
 <artifactId>qiniu-java-sdk</artifactId>
 <version>[7.2.0, 7.2.99]</version>
</dependency>

1.3.2 文件上传

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
 @Test
   public void testUploadImage() {
       Configuration cfg = new Configuration(Zone.zone0());
       UploadManager uploadManager = new UploadManager(cfg);
       String accessKey = "COuoDRVa7JLsuurzIvQSI_pEDceHDw3yGfJEmvwv";
       String secretKey = "3RWpTjB5Jxg3QosUFr4mxbHXJ5JR2m6AHQqYsSlr";
       String bucket = "test-bucket";
       String localFilePath = "C:\\Users\\ThinkPad\\Desktop\\ihrm\\day9\\资源\\照片
\\001.png";
       //默认不指定key的情况下,以文件内容的hash值作为文件名
       String key = "test";
       Auth auth = Auth.create(accessKey, secretKey);
       String upToken = auth.uploadToken(bucket);
       try {
           Response response = uploadManager.put(localFilePath, key, upToken);
           //解析上传成功的结果
           DefaultPutRet putRet = new Gson().fromJson(response.bodyString(),
DefaultPutRet.class);
           System.out.println(response.bodyString());
      } catch (QiniuException ex) {
           Response r = ex.response;
           System.err.println(r.toString());
           try {
               System.err.println(r.bodyString());
          } catch (QiniuException ex2) {
               //ignore
          }
      }
  }

1.3.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
 @Test
   public void testUploadImage1() {
       Configuration cfg = new Configuration(Zone.zone0());
       String accessKey = "COuoDRVa7JLsuurzIvQSI_pEDceHDw3yGfJEmvwv";
       String secretKey = "3RWpTjB5Jxg3QosUFr4mxbHXJ5JR2m6AHQqYsSlr";
       String bucket = "test-bucket";
       String key = "test";
       Auth auth = Auth.create(accessKey, secretKey);
       String upToken = auth.uploadToken(bucket);
       String localFilePath = "C:\\Users\\ThinkPad\\Desktop\\ihrm\\day9\\资源
\\test.xlsx";
       String localTempDir = Paths.get(System.getProperty("java.io.tmpdir"),
bucket).toString();
       System.out.println(localTempDir);
       try {
           //设置断点续传文件进度保存目录
           FileRecorder fileRecorder = new FileRecorder(localTempDir);
           UploadManager uploadManager = new UploadManager(cfg, fileRecorder);
           try {
               Response response = uploadManager.put(localFilePath, key, upToken);
               //解析上传成功的结果
               DefaultPutRet putRet = new Gson().fromJson(response.bodyString(),
DefaultPutRet.class);
               System.out.println(putRet.key);
               System.out.println(putRet.hash);
          } catch (QiniuException ex) {
               Response r = ex.response;
               System.err.println(r.toString());
               try {
                   System.err.println(r.bodyString());
              } catch (QiniuException ex2) {
                   //ignore
              }
          }
          } catch (IOException ex) {
           ex.printStackTrace();
      }
  }

1.4 文件下载

对于公开空间,其访问的链接主要是将空间绑定的域名(可以是七牛空间的默认域名或者是绑定的自定义域名)拼接上空间里面的文件名即可访问,标准情况下需要在拼接链接之前,将文件名进行urlencode以兼容不同的字符。

1.5 七牛云实现用户头像上传

( 1 )创建文件上传的工具类

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
public class QiniuUploadUtil {
   private static final String accessKey = "COuoDRVa7JLsuurzIvQSI_pEDceHDw3yGfJEmvwv";
   private static final String secretKey = "3RWpTjB5Jxg3QosUFr4mxbHXJ5JR2m6AHQqYsSlr";
   private static final String bucket = "test-bucket";
   private static final String prix = "http://pk9vj7em6.bkt.clouddn.com/";
   private UploadManager manager;
   public QiniuUploadUtil() {
       //初始化基本配置
       Configuration cfg = new Configuration(Zone.zone0());
       //创建上传管理器
       manager = new UploadManager(cfg);
  }
   public String upload(String imgName , byte [] bytes) {
       Auth auth = Auth.create(accessKey, secretKey);
       //构造覆盖上传token
       String upToken = auth.uploadToken(bucket,imgName);
       try {
           Response response = manager.put(bytes, imgName, upToken);
           DefaultPutRet putRet = new Gson().fromJson(response.bodyString(),
DefaultPutRet.class);
           //返回请求地址
           return prix+putRet.key+"?t="+new Date().getTime();
      } catch (Exception ex) {
           ex.printStackTrace();
      }
       return null;
  }
}

( 2 )使用七牛云实现用户头像上传

修改UserService方法

1
2
3
4
5
6
7
8
9
public String uploadImage(String id, MultipartFile file) throws Exception {
       User user = userDao.findById(id).get();
       String key = new QiniuUploadUtil().upload(user.getId(), file.getBytes());
       if(key != null) {
           user.setStaffPhoto(key);
           userDao.save(user);
      }
       return key;
  }

3 PDF报表打印概述

3.1 概述

在企业级应用开发中,报表生成、报表打印下载是其重要的一个环节。在之前的课程中我们已经学习了报表中比较重要的一种:Excel报表。其实除了Excel报表之外,PDF报表也有广泛的应用场景,必须用户详细资料,用户简历等。接下来的课程,我们就来共同学习PDF报表

3.2 常见PDF报表的制作方式

目前世面上比较流行的制作PDF报表的工具如下:

  1. iText PDF:iText是著名的开放项目,是用于生成PDF文档的一个java类库。通过iText不仅可以生成PDF或rtf的文档,而且可以将XML、Html文件转化为PDF文件。

  2. Openoffice:openoffice是开源软件且能在windows和linux平台下运行,可以灵活的将word或者Excel转化为PDF文档。

  3. Jasper Report:是一个强大、灵活的报表生成工具,能够展示丰富的页面内容,并将之转换成PDF

3.3 JasperReport框架的介绍

image-20220922111309449

JasperReport是一个强大、灵活的报表生成工具,能够展示丰富的页面内容,并将之转换成PDF,HTML,或者XML格式。该库完全由Java写成,可以用于在各种Java应用程序,包括J2EE,Web应用程序中生成动态内容。只需要将JasperReport引入工程中即可完成PDF报表的编译、显示、输出等工作。

  • 在开源的JAVA报表工具中,JASPER Report发展是比较好的,比一些商业的报表引擎做得还好,如支持了十字交叉报表、统计报表、图形报表,支持多种报表格式的输出,如PDF、RTF、XML、CSV、XHTML、TEXT、DOCX以及OpenOffice。
  • 数据源支持更多,常用 JDBC SQL查询、XML文件、CSV文件 、HQL(Hibernate查询),HBase,JAVA集合等。还允许你义自己的数据源,通过JASPER文件及数据源,JASPER就能生成最终用户想要的文档格式。

4 JasperReport的开发步骤

4.1 JasperReport生命周期

通常我们提到PDF报表的时候,浮现在脑海中的是最终的PDF文档文件。在JasperReports中,这只是报表生命周期的最后阶段。通过JasperReports生成PDF报表一共要经过三个阶段,我们称之为 JasperReport的生命周期,这三个阶段为:设计(Design)阶段、执行(Execution)阶段以及输出(Export)阶段,如下图所示:

image-20220922111356091

  1. 设计阶段(Design):所谓的报表设计就是创建一些模板,模板包含了报表的布局与设计,包括执行计算的复杂公式、可选的从数据源获取数据的查询语句、以及其它的一些信息。模板设计完成之后,我们将模板保存为JRXML文件(JR代表JasperReports),其实就是一个XML文件。

  2. 执行阶段(Execution):使用以JRXML文件编译为可执行的二进制文件(即.Jasper文件)结合数据进行执行,填充报表数据

  3. 输出阶段(Export):数据填充结束,可以指定输出为多种形式的报表

4.2 JasperReport原理简述

image-20220922111403715

  1. JRXML:报表填充模板,本质是一个XML.

JasperReport已经封装了一个dtd,只要按照规定的格式写这个xml文件,那么jasperReport就可以将其解析最终生成报表,但是jasperReport所解析的不是我们常见的.xml文件,而是.jrxml文件,其实跟xml是一样的,只是后缀不一样。

  1. Jasper:由JRXML模板编译生成的二进制文件,用于代码填充数据。

解析完成后JasperReport就开始编译.jrxml文件,将其编译成.jasper文件,因为JasperReport只可以对.jasper文件进行填充数据和转换,这步操作就跟我们java中将java文件编译成class文件是一样的

  1. Jrprint:当用数据填充完Jasper后生成的文件,用于输出报表。

这一步才是JasperReport的核心所在,它会根据你在xml里面写好的查询语句来查询指定是数据库,也可以控制在后台编写查询语句,参数,数据库。在报表填充完后,会再生成一个.jrprint格式的文件(读取jasper文件进行填充,然后生成一个jrprint文件)

  1. Exporter:决定要输出的报表为何种格式,报表输出的管理类。

  2. Jasperreport可以输出多种格式的报表文件,常见的有Html,PDF,xls等

4.3 开发流程概述

  • 制作报表模板
  • 模板编译
  • 构造数据
  • 填充模板数据

5 模板工具Jaspersoft Studio

5.1 概述

Jaspersoft Studio是JasperReports库和JasperReports服务器的基于Eclipse的报告设计器; 它可以作为Eclipse插件或作为独立的应用程序使用。Jaspersoft Studio允许您创建包含图表,图像,子报表,交叉表等的复杂布局。您可以通过JDBC,TableModels,JavaBeans,XML,Hibernate,大数据(如Hive),CSV,XML / A以及自定义来源等各种来源访问数据,然后将报告发布为PDF,RTF, XML,XLS,CSV,HTML,XHTML,文本,DOCX或OpenOffice。

Jaspersoft Studio 是一个可视化的报表设计工具,使用该软件可以方便地对报表进行可视化的设计,设计结果为格式.jrxml 的 XML 文件,并且可以把.jrxml 文件编译成.jasper 格式文件方便 JasperReport 报表引擎解析、显示。

5.2 安装配置

到JasperReport官网下载 https://community.jaspersoft.com/community-download

image-20220922111444285

下载 Library Jar包(传统导入jar包工程需下载)和模板设计器Jaspersoft studio。并安装Jaspersoft studio,安装的过程比较简单,一直下一步直至安装成功即可。

5.3 面板介绍

image-20220922111453622

  • Report editing area (主编辑区域)中,您直观地通过拖动,定位,对齐和通过 Designer palette(设计器调色板)对报表元素调整大小。JasperSoft Studio 有一个多标签编辑器,Design,Source 和 Preview:

    • Design tab:当你打开一个报告文件,它允许您以图形方式创建报表选中

    • Source tab: 包含用于报表的 JRXML 源代码。

    • Preview tab: 允许在选择数据源和输出格式后,运行报表预览。

  • Repository Explorer view:包含 JasperServer 生成的连接和可用的数据适配器列表

  • Project Explorer view:包含 JasperReports 的工程项目清单

  • Outline view:在大纲视图中显示了一个树的形式的方式报告的完整结构。

  • Properties view:通常是任何基于 Eclipse 的产品/插件的基础之一。它通常被填充与实际所选元素的属性的信息。这就是这样,当你从主设计区域(即:一个文本字段)选择一个报表元素或从大纲,视图显示了它的信息。其中一些属性可以是只读的,但大部分都是可编辑的,对其进行修改,通常会通知更改绘制的元素(如:元素的宽度或高度)。

  • Problems view:显示的问题和错误,例如可以阻断报告的正确的编译。

  • Report state summary 提供了有关在报表编译/填充/执行统计用户有用的信息。错误会显示在这里

5.4 基本使用

5.4.1 模板制作

( 1 )打开Jaspersoft Studio ,新建一个project, 步骤: File -> New -> Project-> JasperReports Project

image-20220922111541254

( 2 )新建一个Jasper Report模板,在 Stidio的左下方Project Explorer 找到刚才新建的Project (我这里新建的是DemoReport),步骤:项目右键 -> New -> Jasper Report

image-20220922111550906

( 3 )选择 Blank A4 (A4纸大小的模板),然后 Next 命名为DemoReport1.jrxml.

image-20220922111621801

如图所示,报表模板被垂直的分层,每一个部分都是一个Band,每一个Band的特点不同:

image-20220922111633833

  • Title(标题):只在整个报表的第一页的最上端显示。只在第一页显示,其他页面均不显示。
  • Page Header(页头):在整个报表中每一页都会显示。在第一页中,出现的位置在 Title Band的下面。在除了第一页的其他页面中Page Header 的内容均在页面的最上端显示。
  • Page Footer(页脚):在整个报表中每一页都会显示。显示在页面的最下端。一般用来显示页码。
  • Detail 1(详细):报表内容,每一页都会显示。
  • Column Header(列头):Detail中打印的是一张表的话,这Column Header就是表中列的列头。
  • Column Footer(列脚):Detail中打印的是一张表的话,这Column Footer就是表中列的列脚。
  • Summary(统计):表格的合计段,出现在整个报表的最后一页中,在Detail 1 Band后面。主要是用来做报表的合计显示。

5.4.2 编译模板

右键单机模板文件 -> compile Report 对模板进行编译,生成.jasper文件

image-20220922111652287

5.4.3 整合工程

( 1 )新建SpringBoot工程引入坐标

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
 <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.0.5.RELEASE</version>
       <relativePath/>
   </parent>
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-thymeleaf</artifactId>
       </dependency>
       <dependency>
           <groupId>net.sf.jasperreports</groupId>
           <artifactId>jasperreports</artifactId>
           <version>6.5.0</version>
       </dependency>
       <dependency>
           <groupId>org.olap4j</groupId>
           <artifactId>olap4j</artifactId>
           <version>1.2.0</version>
       </dependency>
       <dependency>
           <groupId>com.lowagie</groupId>
           <artifactId>itext</artifactId>
           <version>2.1.7</version>
       </dependency>
       <dependency>
           <groupId>org.apache.poi</groupId>
           <artifactId>poi</artifactId>
           <version>4.0.1</version>
       </dependency>
       <dependency>
           <groupId>org.apache.poi</groupId>
           <artifactId>poi-ooxml</artifactId>
           <version>4.0.1</version>
       </dependency>
       <dependency>
           <groupId>org.apache.poi</groupId>
           <artifactId>poi-ooxml-schemas</artifactId>
           <version>4.0.1</version>
       </dependency>
   </dependencies>

( 2 )引入配置文件

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8181
spring:
application:
name: jasper-demo #指定服务名
resources:
static-locations: classpath:/templates/
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/ihrm?useUnicode=true&characterEncoding=utf8
username: root
password: 111111

( 3 )创建启动类

1
2
3
4
5
6
@SpringBootApplication(scanBasePackages = "cn.itcast")
public class JasperApplication {
   public static void main(String[] args) {
       SpringApplication.run(JasperApplication.class, args);
  }
}

( 4 )导入生成的.jasper文件

( 5 )创建测试controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class JasperController {
   @GetMapping("/testJasper")
   public void createHtml(HttpServletResponse response, HttpServletRequest
request)throws Exception{
       //引入jasper文件。由JRXML模板编译生成的二进制文件,用于代码填充数据
       Resource resource = new ClassPathResource("templates/test01.jasper");
       //加载jasper文件创建inputStream
       FileInputStream isRef = new FileInputStream(resource.getFile());
       ServletOutputStream sosRef = response.getOutputStream();
       try {
           //创建JasperPrint对象
           JasperPrint jasperPrint = JasperFillManager.fillReport(isRef, new HashMap<>
(),new JREmptyDataSource());
           //写入pdf数据
           JasperExportManager.exportReportToPdfStream(jasperPrint,sosRef);
      } finally {
           sosRef.flush();
           sosRef.close();
      }
  }
}

5.4.4 中文处理

( 1 )设计阶段需要指定中文样式

image-20220922111757399

( 2 )通过手动指定中文字体的形式解决中文不现实

添加properties文件:

1
2
3
net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperrepor
ts.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.lobstertwo=stsong/fonts.xml

指定中文配置文件fonts.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<fontFamilies>
   <!--<fontFamily name="Lobster Two">-->
   <!--<normal>lobstertwo/LobsterTwo-Regular.otf</normal>-->
   <!--<bold>lobstertwo/LobsterTwo-Bold.otf</bold>-->
   <!--<italic>lobstertwo/LobsterTwo-Italic.otf</italic>-->
   <!--<boldItalic>lobstertwo/LobsterTwo-BoldItalic.otf</boldItalic>-->
   <!--<pdfEncoding>Identity-H</pdfEncoding>-->
   <!--<pdfEmbedded>true</pdfEmbedded>-->
   <!--<!–-->
   <!--<exportFonts>-->
   <!--<export key="net.sf.jasperreports.html">'Lobster Two', 'Times New Roman',
Times, serif</export>-->
   <!--</exportFonts>-->
   <!--–>-->
   <!--</fontFamily>-->
   <fontFamily name="华文宋体">
       <normal>stsong/stsong.TTF</normal>
       <bold>stsong/stsong.TTF</bold>
       <italic>stsong/stsong.TTF</italic>
       <boldItalic>stsong/stsong.TTF</boldItalic>
       <pdfEncoding>Identity-H</pdfEncoding>
       <pdfEmbedded>true</pdfEmbedded>
       <exportFonts>
           <export key="net.sf.jasperreports.html">'华文宋体', Arial, Helvetica, sansserif</export>
           <export key="net.sf.jasperreports.xhtml">'华文宋体', Arial, Helvetica, sansserif</export>
       </exportFonts>
       <!--
       <locales>
           <locale>en_US</locale>
           <locale>de_DE</locale>
       </locales>
       -->
   </fontFamily>
</fontFamilies>

引入字体库stsong.TTF

第 10 章 用户档案PDF报表

  • 理解数据填充的两种方式

  • 熟练构造分组报表

  • 熟练构造Chart图形报表

  • 实现个人档案的PDF输出

1 数据填充

我们介绍了如何使用JasperReport来生成简单的文本报表,正式企业开发中动态数据展示也是报表中最重要的一环,接下来我们共同研究的就是填充动态数据到PDF报表中。

1
2
3
4
5
6
7
8
/**
* 填充数据构造JasperPrint
* is: 文件输入流
* parameters:参数
* dataSource:数据源
*/
public static JasperPrint fillReport(InputStream is, Map<String, Object> parameters,
JRDataSource dataSource) throws JRException {

通过这段填充数据的源代码得知,JasperReport对报表模板中的数据填充有很多中方式,最典型的有以下两种:

  • Parameters(参数)填充
  • DataSource(数据源)填充

1.1 参数Map填充数据

Parameters通常是用来在打印的时候从程序里传值到报表里。也就是说parameters通常的是起参数传递的作用。

他们可以被用在一些特定的场合(比如应用中SQL 查询的条件),如report中任何一个需要从外部传入的变量等(如一个

Image对象所包括的char或报表title的字符串)。parameters也需要在创建的时候定义它的数据类型。parameters

的数据类型是标准的java的Object。

1.1.1 模板制作

( 1 ) 创建新模板,删除不需要的Band

image-20220922111918276

( 2 )创建Parameter

在outline面板中找到Parameters,右键 -> Create Parameter,新建一个Parameter(生成一个Paramerter1)

image-20220922111923171

右键 Paramete1 -> Show Properties. 设置Name为title、Class为java.lang.String.这里要注意名字要认真取不能重复,因为传入的参数的key就是这个参数名,以此来进行一一对应

image-20220922111937628

( 3 )模板参数设置

将设置好的参数直接拖入表格中对应的位置,并设置好大小与对齐方式。

image-20220922111943654

1.1.2 PDF输出

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
 @GetMapping("/testJasper02")
   public void createPdf(HttpServletRequest request, HttpServletResponse response)
throws IOException {
       //1.引入jasper文件
       Resource resource = new ClassPathResource("templates/parametersTest.jasper");
       FileInputStream fis = new FileInputStream(resource.getFile());
       //2.创建JasperPrint,向jasper文件中填充数据
       ServletOutputStream os = response.getOutputStream();
       try {
           /**
            * parameters集合中传递的key需要和设计模板中使用的name一致
            */
           HashMap parameters = new HashMap();
           parameters.put("title","用户详情");
           parameters.put("username","李四");
           parameters.put("companyName","传智播客");
           parameters.put("mobile","120");
           parameters.put("departmentName","讲师");
            JasperPrint print = JasperFillManager.fillReport(fis, parameters,new
JREmptyDataSource());
           //3.将JasperPrint已PDF的形式输出
           JasperExportManager.exportReportToPdfStream(print,os);
           response.setContentType("application/pdf");
      } catch (JRException e) {
           e.printStackTrace();
      }finally {
           os.flush();
      }
  }

1.2 数据源填充数据

1.2.1 JDBC数据源

1.2.1.1 配置数据连接

使用JDBC数据源填充数据:使用Jaspersoft Studio 先要配置一个数据库连接

填写数据源的类型,选择“Database JDBC Connection”

image-20220922112016108

配置数据库信息

image-20220922112024955

这一步,需要: ( 1 )给创建的这个数据连接起个名字; ( 2 )根据数据库选择驱动类型; Jaspersoft Studio 已经内置了很多常用数据库的驱动,使用的时候直接选就可以了。当然,如果这还满足不了你的话,你还可以添加你指定的 JDBC 驱动 jar 包。

1.2.1.2 模板制作

( 1 )制作空白模板

创建空白模板,并将不需要的Band

( 2 )将数据库用户字段配置到模块中

为了方便的进行模板制作,可以将需要数据库表中的字段添加到Studio中。在outline中右键模板,选择dataset and query

image-20220922112126029

用户可以在 SQL 查询语句输入窗口中,输入需要查询数据的查询语句,点击右上角的“Read Fields”按钮,界面下方的字段列表中,就会显示此查询语句中所涵盖的所有字段的列表。在后面的报表设计中,我们就可以直接使用这些字段了。

image-20220922112133325

在“Fields”列表中,只保留报表中使用的字段,其他用不到的字段最好用“Delete”删掉,防止由于数据表变化,导致报表模板中的字段设置与数据表对应不上,导致报表报错。输入完毕后,点击“OK”按钮,系统即会把查询语句保存在报表模板中。

image-20220922112142440

( 3 )填充Filed

将id,mobile,username等拖入到 Detail Band中设计模板如下:

image-20220922112155999

1.2.1.3 PDF输出

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
//测试JDBC连接数据源
   @GetMapping("/testJasper03")
   public void createPdf(HttpServletRequest request, HttpServletResponse response)
throws Exception {
       //1.引入jasper文件
       Resource resource = new ClassPathResource("templates/testConn.jasper");
       FileInputStream fis = new FileInputStream(resource.getFile());
       //2.创建JasperPrint,向jasper文件中填充数据
       ServletOutputStream os = response.getOutputStream();
       try {
           /**
            * 1.jasper文件流
            * 2.参数列表
            * 3.数据库连接
            */
           HashMap parameters = new HashMap();
           JasperPrint print = JasperFillManager.fillReport(fis,
parameters,getConnection());
           //3.将JasperPrint已PDF的形式输出
           JasperExportManager.exportReportToPdfStream(print,os);
           response.setContentType("application/pdf");
      } catch (JRException e) {
           e.printStackTrace();
      }finally {
           os.flush();
      }
  }
   //创建数据库Connection
   public Connection getConnection() throws Exception {
       String url = "jdbc:mysql://localhost/ihrm";
       Class.forName("com.mysql.jdbc.Driver");
       Connection conn = DriverManager.getConnection(url, "root", "111111");
       return conn;
  }

1.2.2 JavaBean数据源

1.2.2.1 创建Filed

( 1 )创建Filed

image-20220922112239079

( 2 )构造模板

image-20220922112256055

1.2.2.2 PDF输出

( 1 )配置实体类

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
package cn.itcast.bean;
public class User {
   private String id;
   private String username;
   private String mobile;
   private String companyName;
   private String departmentName;
   public User(String id, String username, String mobile, String companyName, String
departmentName) {
       this.id = id;
       this.username = username;
       this.mobile = mobile;
       this.companyName = companyName;
       this.departmentName = departmentName;
  }
   public String getId() {
       return id;
  }
   public void setId(String id) {
       this.id = id;
  }
   public String getUsername() {
       return username;
  }
  public void setUsername(String username) {
       this.username = username;
  }
   public String getMobile() {
       return mobile;
  }
   public void setMobile(String mobile) {
       this.mobile = mobile;
  }
   public String getCompanyName() {
       return companyName;
  }
   public void setCompanyName(String companyName) {
       this.companyName = companyName;
  }
   public String getDepartmentName() {
       return departmentName;
  }
   public void setDepartmentName(String departmentName) {
       this.departmentName = departmentName;
  }
}

(2)使用javaBean数据源

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
//测试javaBean数据源
   @GetMapping("/testJasper04")
   public void createPdf(HttpServletRequest request, HttpServletResponse response)
throws Exception {
       //1.引入jasper文件
       Resource resource = new ClassPathResource("templates/testJavaBean.jasper");
       FileInputStream fis = new FileInputStream(resource.getFile());
       //2.创建JasperPrint,向jasper文件中填充数据
       ServletOutputStream os = response.getOutputStream();
       try {
           HashMap parameters = new HashMap();
           //构造javaBean数据源
           JRBeanCollectionDataSource ds = new
JRBeanCollectionDataSource(getUserList());
           /**
            * 1.jasper文件流
            * 2.参数列表
            * 3.JRBeanCollectionDataSource
            */
           JasperPrint print = JasperFillManager.fillReport(fis, parameters,ds);
           //3.将JasperPrint已PDF的形式输出
           JasperExportManager.exportReportToPdfStream(print,os);
           response.setContentType("application/pdf");
      } catch (JRException e) {
           e.printStackTrace();
      }finally {
           os.flush();
      }
  }
   //创建数据库Connection
   public List<User> getUserList() throws Exception {
       List<User> list = new ArrayList<>();
       for (int i=1;i<=5;i++) {
           User user = new User(i+"", "testName"+i, "10"+i, "企业"+i, "部门"+i);
           list.add(user);
      }
       return list;
  }

2 分组报表

2.1 概述

有两种情况会使用分组报表:

  • 美观和好看的显示。
  • 当数据分为两层表时,经常需要批量打印子表的数据。打印时,常常需要按照父表的外键或关联值进行自动分组,即每一条父表记录所属的子表记录打印到一组报表中,每组报表都单独计数及计算页数。

在应用中,可以通过选择需要打印的父表记录,将父表记录的 ID 传入,由报表自动进行分组。

2.2 设置分组属性

( 1 )新建模板

使用用户列表模板完成分组案例

( 2 )新建报表群组

选中报表名称点击右键,选择菜单中的“Create Group”。

image-20220922112415590

需要设置分组的名称、分组字段。也可以设置按照指定的函数、方法处理后进行分组

image-20220922112626360

按照字段“companyName”进行分组。设置完毕,点击“Next”。系统显示细节设置界面。此处可以设置是否加入“group header”和“group footer”区。建议保持默认选中,加入这两个区域,这样可以控制在每组报表的结尾,打印相应的信息,例如统计信息等。

image-20220922112638431

( 3 )放置报表数据

将companyName拖入 Group Header中 ,会跳出 TextField Wizard框,选中 NoCalculation Function

image-20220922112655825

双击 $F{deptId} 会弹出Expression editor框

image-20220922112703287

2.3 添加分组Band

将需要作为表头打印的内容拖入 CompanyGroup Header1 栏,将字段拖入 detail 栏,将每个分组结尾需要打印的内容放入 Companygroup footer 栏,将页脚需要打印的内容放入 Page Footer栏,如下图。

image-20220922112718048

2.4 PDF输出

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
//测试分组
   @GetMapping("/testJasper05")
   public void createPdf(HttpServletRequest request, HttpServletResponse response)
throws Exception {
       //1.引入jasper文件
       Resource resource = new ClassPathResource("templates/testGroup.jasper");
       FileInputStream fis = new FileInputStream(resource.getFile());
       //2.创建JasperPrint,向jasper文件中填充数据
       ServletOutputStream os = response.getOutputStream();
       try {
           HashMap parameters = new HashMap();
           //构造javaBean数据源
           JRBeanCollectionDataSource ds = new
JRBeanCollectionDataSource(getUserList());
           /**
            * 1.jasper文件流
            * 2.参数列表
            * 3.JRBeanCollectionDataSource
            */
           JasperPrint print = JasperFillManager.fillReport(fis, parameters,ds);
           //3.将JasperPrint已PDF的形式输出
           JasperExportManager.exportReportToPdfStream(print,os);
           response.setContentType("application/pdf");
      } catch (JRException e) {
           e.printStackTrace();
      }finally {
           os.flush();
      }
  }
   //创建数据库Connection
   public List<User> getUserList() throws Exception {
   List<User> list = new ArrayList<>();
       for(int i=1;i<=3;i++) {
           User user = new User("it00"+i, "itcast"+i, "1380000000"+i, "传智播客", "讲 师");
           list.add(user);
      }
       for(int i=1;i<=3;i++) {
           User user = new User("hm00"+i, "itheima"+i, "1880000000"+i, "黑马程序员", "讲 师");
           list.add(user);
      }
       return list;
  }

效果如下:

image-20220922112751933

3 Chart图表

3.1 创建模板

( 1 )创建模板,删除不需要的band,保留title和summary。

image-20220922112814188

( 2 )创建fileds

image-20220922112821923

( 3 )创建chart图标

第一步:palette面板找到chart图表,拖拽到band中

第二步:选择需要的图表类型

image-20220922112830106

第三步:设置图表参数

image-20220922112835043

  • Key: 圆饼图的内容是什么,也就是下面的 First,Second…的内容
  • Value:这个圆饼图的比例依据,根据 Value 属性来显示每个 Key 占的比例
  • Label:显示标签

3.2 PDF输出

3.2.1 实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserCount {
   private String companyName;
   private Integer count;
   public UserCount(String companyName, Integer count) {
       this.companyName = companyName;
       this.count = count;
  }
   public String getCompanyName() {
       return companyName;
  }
   public void setCompanyName(String companyName) {
       this.companyName = companyName;
  }
   public Integer getCount() {
       return count;
       }
   public void setCount(Integer count) {
       this.count = count;
  }
}

3.2.2 PDF输出

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
//测试图表
   @GetMapping("/testJasper06")
   public void createPdf(HttpServletRequest request, HttpServletResponse response)
throws Exception {
       //1.引入jasper文件
       Resource resource = new ClassPathResource("templates/testChart.jasper");
       FileInputStream fis = new FileInputStream(resource.getFile());
       //2.创建JasperPrint,向jasper文件中填充数据
       ServletOutputStream os = response.getOutputStream();
       try {
           HashMap parameters = new HashMap();
           //parameters.put("userCountList",getUserList());
           //构造javaBean数据源
           JRBeanCollectionDataSource ds = new
JRBeanCollectionDataSource(getUserList());
           /**
            * 1.jasper文件流
            * 2.参数列表
            * 3.JRBeanCollectionDataSource
            */
           JasperPrint print = JasperFillManager.fillReport(fis, parameters,ds);
           //3.将JasperPrint已PDF的形式输出
           JasperExportManager.exportReportToPdfStream(print,os);
           response.setContentType("application/pdf");
      } catch (JRException e) {
           e.printStackTrace();
      }finally {
           os.flush();
      }
  }
   //创建数据库Connection
   public List<UserCount> getUserList() throws Exception {
       List<UserCount> list = new ArrayList<>();
       UserCount uc1 = new UserCount("传智播客",10);
       UserCount uc2 = new UserCount("黑马程序员",10);
       list.add(uc1);
       list.add(uc2);
       return list;
  }

4 父子报表

4.1 概述

复杂报表或数据内容较多的时候,可以使用子报表解决。

4.2 制作父报表

首先制作父报表,就是调用子报表的一个基础报表。主报表的作用有如下两种:

  • 父报表中需要显示数据,使用子报表弥补studio设计的不足
  • 父报表不需要显示任何数据,只是作为子报表的载体。适用于复杂报表的设计

4.3 制作子报表

点击组件面板上的“Subreport”按钮,拖动到报表工作区上。

image-20220922112947372

系统会自动弹出子报表选择窗口。可以选择创建一个新报表,还是使用一个已有的报表作为子报表。

image-20220922112952783

选择“Create a new report”,可以立即制作新的子报表;如果选择“Select an existing report”,则可以调用已经有的报表作为子报表;如果选择“Just create the subreport element”,系统会生成一个子报表区,可以在之后挂接需要的子报表。

4.4 参数传递

image-20220922113001993

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
//测试父子模板
   @GetMapping("/testJasper07")
   public void createPdf(HttpServletRequest request, HttpServletResponse response)
throws Exception {
       //1.引入jasper文件
       Resource resource = new ClassPathResource("templates/main.jasper");
       FileInputStream fis = new FileInputStream(resource.getFile());
       //2.创建JasperPrint,向jasper文件中填充数据
       ServletOutputStream os = response.getOutputStream();
       try {
           HashMap parameters = new HashMap();
           Resource subResource = new ClassPathResource("templates/sub-group.jasper");
           parameters.put("subpath",subResource.getFile().getPath());
           parameters.put("sublist",getUserList());
           parameters.put("sublist",getUserList());
           JRBeanCollectionDataSource ds = new
JRBeanCollectionDataSource(getUserList());
           JasperPrint print = JasperFillManager.fillReport(fis, parameters,new
JREmptyDataSource());
           //3.将JasperPrint已PDF的形式输出
           JasperExportManager.exportReportToPdfStream(print,os);
           response.setContentType("application/pdf");
      } catch (JRException e) {
           e.printStackTrace();
      }finally {
           os.flush();
      }
  }
   //创建数据库Connection
   public List<User> getUserList() throws Exception {
       List<User> list = new ArrayList<>();
       for(int i=1;i<=3;i++) {
           User user = new User("it00"+i, "itcast"+i, "1380000000"+i, "传智播客", "讲 师");
           list.add(user);
      }
       for(int i=1;i<=3;i++) {
           User user = new User("hm00"+i, "itheima"+i, "1880000000"+i, "黑马程序员", "讲 师");
           list.add(user);
      }
       return list;
  }

5 用户档案下载

5.1 搭建环境

(1) 配置坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
           <groupId>net.sf.jasperreports</groupId>
           <artifactId>jasperreports</artifactId>
           <version>6.5.0</version>
       </dependency>
       <dependency>
           <groupId>org.olap4j</groupId>
           <artifactId>olap4j</artifactId>
           <version>1.2.0</version>
       </dependency>
       <dependency>
           <groupId>com.lowagie</groupId>
           <artifactId>itext</artifactId>
           <version>2.1.7</version>
       </dependency>

(2)解决乱码问题

1
2
3
net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperrepor
ts.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.lobstertwo=stsong/fonts.xml

5.2 实现用户档案下载

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
/**
    * 打印员工pdf报表x
    */
   @RequestMapping(value="/{id}/pdf",method = RequestMethod.GET)
   public void pdf(@PathVariable String id) throws IOException {
       //1.引入jasper文件
       Resource resource = new ClassPathResource("templates/profile.jasper");
       FileInputStream fis = new FileInputStream(resource.getFile());
       //2.构造数据
       //a.用户详情数据
       UserCompanyPersonal personal = userCompanyPersonalService.findById(id);
       //b.用户岗位信息数据
       UserCompanyJobs jobs = userCompanyJobsService.findById(id);
       //c.用户头像       域名 / id
       String staffPhoto = "http://pkbivgfrm.bkt.clouddn.com/"+id;
       System.out.println(staffPhoto);
       //3.填充pdf模板数据,并输出pdf
       Map params = new HashMap();
       Map<String, Object> map1 = BeanMapUtils.beanToMap(personal);
       Map<String, Object> map2 = BeanMapUtils.beanToMap(jobs);
       params.putAll(map1);
       params.putAll(map2);
       params.put("staffPhoto","staffPhoto");
       ServletOutputStream os = response.getOutputStream();
       try {
           JasperPrint print = JasperFillManager.fillReport(fis, params,new
JREmptyDataSource());
           JasperExportManager.exportReportToPdfStream(print,os);
      } catch (JRException e) {
           e.printStackTrace();
      }finally {
           os.flush();
      }
  }

第 11 章 刷脸登录

  • 理解刷脸登录的需求
  • 理解刷脸登录的开发流程
  • 实现刷脸登录功能

1 浅谈人工智能

1.1 人工智能的概述

人工智能(Artificial Intelligence),英文缩写为AI。它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学

人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器,该领域的研究包括机器人、语言识别、图像识别、自然语言处理和专家系统等。人工智能从诞生以来,理论和技术日益成熟,应用领域也不断扩大,可以设想,未来人工智能带来的科技产品,将会是人类智慧的“容器”。人工智能可以对人的意识、思维的信息过程的模拟。人工智能不是人的智能,但能像人那样思考、也可能超过人的智能。

image-20220922113129637

1.2 人工智能的应用领域

随着智能家电、穿戴设备、智能机器人等产物的出现和普及,人工智能技术已经进入到生活的各个领域,引发越来越多的关注。

image-20220922113139771

1.3 基于人工智能的刷脸登录介绍

刷脸登录是基于人工智能、生物识别、3D传感、大数据风控技术,最新实现的登录形式。用户在无需输入用户名密码的前提下,凭借“刷脸”完成登录过程。实现刷脸登录的核心是人脸处理,在人脸处理中有两个概念:

  • 人脸检测:检测图中的人脸,并为人脸标记出边框。检测出人脸后,可对人脸进行分析,获得眼、口、鼻轮廓等 72 个关键点定位准确识别多种人脸属性,如性别,年龄,表情等信息
  • 人脸识别(对比):通过提取人脸的特征,计算两张人脸的相似度,从而判断是否同一个人,并给出相似度评分。

作为中小型企业,可以采取世面上流行的人工智能产品快速的实现刷脸登录需求。目前比较流行人脸检测产品如下(我们的课程中使用百度云AI来完成人脸登录功能):

  • Face++
  • 腾讯优图
  • 科大讯飞
  • 百度云AI

2 百度云AI概述

2.1 概述

百度人脸识别基于深度学习的人脸识别方案,准确识别图片中的人脸信息,提供如下功能:

  • 人脸检测:精准定位图中人脸,获得眼、口、鼻等 72 个关键点位置,分析性别、年龄、表情等多种人脸属性
  • 人脸对比:对比两张人脸的相似度,并给出相似度评分,从而判断是否同一个人
  • 人脸搜索:针对一张人脸照片,在指定人脸集合中搜索,找出最相似的一张脸或多张人脸,并给出相似度分值
  • 活体检测:提供离线/在线方式的活体检测能力,判断操作用户是否为真人,有效抵御照片、视频、模具等作弊攻击

视频流人脸采集:设备端离线实时监测视频流中的人脸,同时支持处理静态图片或者视频流,输出人脸图片并进行图片质量控制

2.2 百度云AI的开发步骤

  1. 注册账号创建应用

  2. 搭建工程导入依赖

  3. 人脸注册

  4. 人脸识别

2.3 百度云AI的注册与认证

( 1 )注册百度云帐号

打开百度云平台:https://login.bce.baidu.com/reg.html?tpl=bceplat&from=portal进行账号注册

image-20220922113224927

( 2 )激活人脸识别,并创建应用

找到产品-人工智能-人脸识别激活应用,并注册应用

image-20220922113232695

应用创建完成之后,进入刚刚创建的应用获取开发所需的AppID,API Key,Secret Key。

3 百度云API的入门

3.1 搭建环境

创建工程并导入依赖:

1
2
3
4
5
<dependency>
   <groupId>com.baidu.aip</groupId>
   <artifactId>java-sdk</artifactId>
   <version>4.8.0</version>
</dependency>

3.2 人脸注册

用于从人脸库中新增用户,可以设定多个用户所在组,及组内用户的人脸图片

典型应用场景:构建您的人脸库,如会员人脸注册,已有用户补全人脸信息等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  //人脸注册
   @Test
   public void testFaceRegister() throws Exception {
       //传入可选参数调用接口
       HashMap<String, String> options = new HashMap<String, String>();
       options.put("quality_control", "NORMAL");
       options.put("liveness_control", "LOW");
       String imageType = "BASE64";
       String groupId = "itcast";
       String userId = "1000";
        //构造base64图片字符串
       String path = "C:\\Users\\ThinkPad\\Desktop\\ihrm\\day11\\资源\\照片\\001.png";
       byte[] bytes = Files.readAllBytes(Paths.get(path));
       String image = Base64Util.encode(bytes);
       // 人脸注册
       JSONObject res = client.addUser(image, imageType, groupId, userId, options);
       System.out.println(res.toString(2));
  }

人脸注册 请求参数详情

image-20220922113324999

人脸注册 返回数据参数详情

image-20220922113341906

3.3 人脸更新

用于对人脸库中指定用户,更新其下的人脸图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 //人脸更新
   @Test
   public void testFaceUpdate() throws Exception {
       //传入可选参数调用接口
       HashMap<String, String> options = new HashMap<String, String>();
       options.put("quality_control", "NORMAL");
       options.put("liveness_control", "LOW");
       String imageType = "BASE64";
       String groupId = "itcast";
       String userId = "1000";
       //构造base64图片字符串
       String path = "C:\\Users\\ThinkPad\\Desktop\\ihrm\\day11\\资源\\照片\\001.png";
       byte[] bytes = Files.readAllBytes(Paths.get(path));
       String image = Base64Util.encode(bytes);
       //人脸注册
       JSONObject res = client.updateUser(image, imageType, groupId, userId, options);
       System.out.println(res.toString(2));
  }

人脸更新 请求参数详情

image-20220922113413222

人脸更新 返回数据参数详情

image-20220922113427404

3.4 人脸检测

人脸检测:检测图片中的人脸并标记出位置信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
//人脸检测
   @Test
   public void testFaceDetect() throws IOException {
       String path = "C:\\Users\\ThinkPad\\Desktop\\ihrm\\day11\\资源\\照片\\002.png";
       byte[] bytes = Files.readAllBytes(Paths.get(path));
       String image = Base64Util.encode(bytes);
       String imageType = "BASE64";
       HashMap<String, String> subOptions = new HashMap<String, String>();
       subOptions.put("max_face_num", "10");
       //人脸检测
       JSONObject res = client.detect(image, imageType, subOptions);
       System.out.println(res.toString(2));
  }

人脸检测 请求参数详情

image-20220922113502675

人脸检测 返回数据参数详情

image-20220922113522586

image-20220922113532570

image-20220922113552075

image-20220922113602326

3.5 人脸查找

在指定人脸集合中,找到最相似的人脸

1
2
3
4
5
6
7
8
9
10
11
12
13
//人脸搜索
   @Test
   public void testFaceSearch() throws IOException {
       String path = "D:\\223.png";
       byte[] bytes = Files.readAllBytes(Paths.get(path));
       String image = Base64Util.encode(bytes);
       String imageType = "BASE64";
       HashMap<String, String> options = new HashMap<String, String>();
       options.put("user_top_num", "1");
       //人脸搜索
       JSONObject res = client.search(image, imageType, "itcast", options);
       System.out.println(res.toString(2));
  }

人脸搜索 请求参数详情

image-20220922113637310

人脸搜索 返回数据参数详情

image-20220922113648555

4 刷脸登录实现

4.1 需求分析

为了用户登录的便捷,我们在系统中增加刷脸登录的功能,大致流程如下图:

image-20220922113700933

  • 用户在登录页面触发刷脸登录功能

  • 在该页面中弹出一个二维码,此二维码是后台即时生成,包含特殊标志(但本质上是一个URL链接),后续登录流程将会使用此标志。用户对该二维码进行扫描,并在扫描端(手机或PC,注:此处不建议使用微信扫描)浏览器打开落地页。

  • 打开落地页时,授权使用摄像头,并进行人脸识别,识别成功后,关闭落地页。

  • 识别成功后,登录页面自动检测到成功标识,并获取相关信息,进入系统主页。

  • 技术点

    • 二维码生成

    • 百度云AI

    • Redis

    • 前端摄像头调用

4.2 搭建环境

(1) 引入坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 百度云AI API-->
<dependency>
   <groupId>com.baidu.aip</groupId>
   <artifactId>java-sdk</artifactId>
   <version>4.8.0</version>
</dependency>
<!-- 二维码 -->
<dependency>
   <groupId>com.google.zxing</groupId>
   <artifactId>core</artifactId>
   <version>3.2.1</version>
</dependency>
<dependency>
   <groupId>com.google.zxing</groupId>
   <artifactId>javase</artifactId>
   <version>3.2.1</version>
</dependency>

( 2 )添加配置

1
2
3
4
5
6
7
8
ai:
appId: 15191935
apiKey: cyWSHgas93Vtdmt42OwbW8pu
secretKey: yf1GusMvvLBdOnyubfLubNyod9iEDEZW
imageType: BASE
groupId: itcast
qr:
url: https://localhost:8080/#/facelogin

( 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
@Component
public class QRCodeUtil {
   /**
    * 生成Base64 二维码
    */
   public String crateQRCode(String content) throws IOException {
       ByteArrayOutputStream os = new ByteArrayOutputStream();
       try {
           QRCodeWriter writer = new QRCodeWriter();
           BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, 200,
200);
           BufferedImage bufferedImage =
MatrixToImageWriter.toBufferedImage(bitMatrix);
           ImageIO.write(bufferedImage, "png", os);
           //添加图片标识
           return new String("data:image/png;base64," +
Base64.encode(os.toByteArray()));
      } catch (Exception e) {
           e.printStackTrace();
      } finally {
           os.close();
      }
       return null;
  }
}

在QRCodeUtil类头添加 @Component 注解,使用时可通过 @Autowired 来自动装配。

( 4 ) 创建基本的工程结构

在系统微服务中构建基本的Controller代码

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
@RestController
@RequestMapping("/sys/faceLogin")
public class FaceLoginController {
   /**
    * 获取刷脸登录二维码
    *     返回值:QRCode对象(code,image)
    *
    */
   @RequestMapping(value = "/qrcode", method = RequestMethod.GET)
   public Result qrcode() throws Exception {
      return null;
  }
   /**
    * 检查二维码:登录页面轮询调用此方法,根据唯一标识code判断用户登录情况
    *     查询二维码扫描状态
    *         返回值:FaceLoginResult
    *             state :-1,0,1 (userId和token)
    */
   @RequestMapping(value = "/qrcode/{code}", method = RequestMethod.GET)
   public Result qrcodeCeck(@PathVariable(name = "code") String code) throws Exception
{
return null;
  }
   /**
    * 人脸登录:根据落地页随机拍摄的面部头像进行登录
    *         根据拍摄的图片调用百度云AI进行检索查找
    */
   @RequestMapping(value = "/{code}", method = RequestMethod.POST)
   public Result loginByFace(@PathVariable(name = "code") String code,
@RequestParam(name = "file") MultipartFile attachment) throws Exception {
return null;
  }
   /**
    * 图像检测,判断图片中是否存在面部头像
    */
   @RequestMapping(value = "/checkFace", method = RequestMethod.POST)
   public Result checkFace(@RequestParam(name = "file") MultipartFile attachment)
throws Exception {
return null;
  }
}

在系统微服务中构建基本的Service代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class FaceLoginService {
   @Value("${qr.url}")
   private String url;
//创建二维码
   public QRCode getQRCode() throws Exception {
return null;
  }
//根据唯一标识,查询用户是否登录成功
   public FaceLoginResult checkQRCode(String code) {
return null;
  }
//扫描二维码之后,使用拍摄照片进行登录
   public String loginByFace(String code, MultipartFile attachment) throws Exception {
return null;
  }
//构造缓存key
   private String getCacheKey(String code) {
       return "qrcode_" + code;
  }
}

4.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
@Component
public class QRCodeUtil {
   /**
    * 生成Base64 二维码
    */
   public String crateQRCode(String content) throws IOException {
       System.out.println(content);
       ByteArrayOutputStream os = new ByteArrayOutputStream();
       try {
           QRCodeWriter writer = new QRCodeWriter();
           BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, 200,
200);
           BufferedImage bufferedImage =
MatrixToImageWriter.toBufferedImage(bitMatrix);
           ImageIO.write(bufferedImage, "png", os);
           //添加图片标识
           return new String("data:image/png;base64," +
Base64.encode(os.toByteArray()));
      } catch (Exception e) {
           e.printStackTrace();
      } finally {
           os.close();
           }
       return null;
  }
}

在QRCodeUtil类头添加 @Component 注解,使用时可通过 @Autowired 来自动装配。

4.4 封装API

对于百度云AI SDK我们进行一些简单的封装,便于使用时,减少代码冗余。

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
package com.ihrm.system.utils;
import com.baidu.aip.face.AipFace;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
@Component
public class BaiduAiUtil {
   @Value("${ai.appId}")
   private String APP_ID;
   @Value("${ai.apiKey}")
   private String API_KEY;
   @Value("${ai.secretKey}")
   private String SECRET_KEY;
   @Value("${ai.imageType}")
   private String IMAGE_TYPE;
   @Value("${ai.groupId}")
   private String groupId;
   private AipFace client;
   private HashMap<String, String> options = new HashMap<String, String>();
   public BaiduAiUtil() {
       options.put("quality_control", "NORMAL");
       options.put("liveness_control", "LOW");
  }
   @PostConstruct
   public void init() {
       client = new AipFace(APP_ID, API_KEY, SECRET_KEY);
  }
   /**
   * 人脸注册 :将用户照片存入人脸库中
    */
   public Boolean faceRegister(String userId, String image) {
       // 人脸注册
       JSONObject res = client.addUser(image, IMAGE_TYPE, groupId, userId, options);
       Integer errorCode = res.getInt("error_code");
       return errorCode == 0 ? true : false;
  }
   /**
    * 人脸更新 :更新人脸库中的用户照片
    */
   public Boolean faceUpdate(String userId, String image) {
       // 人脸更新
       JSONObject res = client.updateUser(image, IMAGE_TYPE, groupId, userId,
options);
       Integer errorCode = res.getInt("error_code");
       return errorCode == 0 ? true : false;
  }
   /**
    * 人脸检测:判断上传图片中是否具有面部头像
    */
   public Boolean faceCheck(String image) {
       JSONObject res = client.detect(image, IMAGE_TYPE, options);
       if (res.has("error_code") && res.getInt("error_code") == 0) {
           JSONObject resultObject = res.getJSONObject("result");
           Integer faceNum = resultObject.getInt("face_num");
           return faceNum == 1?true:false;
      }else{
           return false;
      }
  }
   /**
    * 人脸查找:查找人脸库中最相似的人脸并返回数据
    *         处理:用户的匹配得分(score)大于80分,即可认为是同一个用户
    */
   public String faceSearch(String image) {
       JSONObject res = client.search(image, IMAGE_TYPE, groupId, options);
       if (res.has("error_code") && res.getInt("error_code") == 0) {
           JSONObject result = res.getJSONObject("result");
           JSONArray userList = result.getJSONArray("user_list");
           if (userList.length() > 0) {
               JSONObject user = userList.getJSONObject(0);
               double score = user.getDouble("score");
               if(score > 80) {
                   return user.getString("user_id");
              }
          }
      }
       return null;
  }
}

在构造方法中,实例化client。通过client,可以调用SDK中包含的各种API。

APP_ID, API_KEY, SECRET_KEY在文中第一段中所述位置获取,如没有正确配置,会直接导致API调用失败。

根据官方文档所示,我们大致创建了faceRegister()、faceUpdate()、faceCheck()、faceSearch()四个方法。

  • 人脸注册 faceRegister(groupId, userId, image)
  • groupId:用于人脸库区分人群标识,自定义即可,人脸库会根据提交的groupId,将用户分组
  • userId:人脸库中的用户标识,同组不可重复,自定义即可(通常为系统中用户的唯一标识)
  • image:Base64 用户图片
  • 人脸更新 faceUpdate(groupId, userId, image)
  • 参数解释同人脸注册
  • 该方法用于发生变化时,更新人脸信息
  • 人脸检测 faceCheck(image)
  • image:Base64 用户图片
  • 该方法用于人脸注册、人脸更新和人脸登录前使用
  • 目前采用的方案是检测出人脸数大于0即可,如需深化需求,可按需扩展
  • 人脸登录 faceSearch(image)
  • image:Base64 用户图片
  • 该方法使用的是百度云AI 人脸搜索方法,目前采用的方式是匹配度最高的结果,即要登录的用户

同样的,在BaiduAiUtil类头添加 @Component 注解,使用时可通过 @Autowired 来自动装配。在API调用后返回

值处理上,进行了简单的解析,如需深化解析,可按需扩展。

4.5 功能实现

完成刷脸登录一共需要我们解决如下 5 个问题:

  • 人脸注册/人脸更新

    在刷脸登录之前,我们首先需要对系统中的用户进行人脸注册,将相关信息提交至人脸库,才可通过人脸识别的相关接口进行刷脸登录操作。当用户相貌变更较大时,可通过人脸更新进行人脸信息更换。

  • 二维码生成

    获取验证码。通过工具生成相关信息后,如特殊标志,将特殊标志写入Redis缓存,并将标记值设为”-1“,我们认定值为”-1“,即为当前标记尚未使用。调用QRCodeUtil.crateQRCode()生成二维码。

  • 二维码检测

    前端获取二维码后,对二维码进行展现,并且前台启动定时器,定时检测特殊标记状态值。当状态值为“1”时,表明登录成功。

  • 人脸检测

    当用户扫码进入落地页,通过落地页打开摄像头,并且定时成像。将成像图片,通过接口提交给后端进行人脸检测。

  • 人脸登录

​ 检测成功后,即进行人脸登录,人脸登录后,改变特殊标记状态值,成功为“1”,失败为“0”。当登录成功时进行自动登录操作,将token和userId存入到redis中。

4.5.1 后端实现

( 1 )人脸注册/人脸更新:在刷脸登录之前,我们首先需要对系统中的用户进行人脸注册,将相关信息提交至人脸库,才可通过人脸识别的相关接口进行刷脸登录操作。当用户相貌变更较大时,可通过人脸更新进行人脸信息更换。

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
//人脸注册
@RequestMapping(value = "/register/face", method = RequestMethod.POST)
   public Boolean registerFace(@RequestParam(name = "fid") String fid) throws
Exception {
       SysFile sysFile = fileService.findById(fid);
       String path = uploadPath + "/" + sysFile.getPath() + "/" +
sysFile.getUuidName();
       byte[] bytes = Files.readAllBytes(Paths.get(path));
       Boolean isSuc;
       String image = Base64Utils.encodeToString(bytes);
       isSuc = userService.checkFace(image);
       if (isSuc) {
           isSuc = baiduAiUtil.faceRegister("1", userId, image);
      }
       return isSuc;
  }
//人脸更新
   @RequestMapping(value = "/update/face", method = RequestMethod.POST)
   public boolean updateFace(@RequestParam(name = "fid") String fid) throws Exception
{
       SysFile sysFile = fileService.findById(fid);
       String path = uploadPath + "/" + sysFile.getPath() + "/" +
sysFile.getUuidName();
       byte[] bytes = Files.readAllBytes(Paths.get(path));
       Boolean isSuc;
       String image = Base64Utils.encodeToString(bytes);
       isSuc = userService.checkFace(image);
       if (isSuc) {
           isSuc = baiduAiUtil.faceUpdate("1", userId, image);
      }
       return isSuc;
  }

( 2 )二维码生成:获取验证码。通过工具生成相关信息后,如特殊标志,将特殊标志写入Redis缓存,并将标记值设为”-1“,我们认定值为”-1“,即为当前标记尚未使用。调用QRCodeUtil.crateQRCode()生成二维码。

Controller:

1
2
3
4
5
6
7
/**
    * 获取刷脸登录二维码
    */
   @RequestMapping(value = "/qrcode", method = RequestMethod.GET)
   public Result qrcode() throws Exception {
       return new Result(ResultCode.SUCCESS, faceLoginService.getQRCode());
  }

Service:

1
2
3
4
5
6
7
8
public QRCode getQRCode() throws Exception {
       String code = idWorker.nextId() + "";
       FaceLoginResult result = new FaceLoginResult("-1");
       redisTemplate.boundValueOps(getCacheKey(code)).set(result, 30,
TimeUnit.MINUTES);
       String strFile = qrCodeUtil.crateQRCode(url + "?code=" + code);
       return new QRCode(code, strFile);
  }

( 3 )二维码检测:前端获取二维码后,对二维码进行展现,并且前台启动定时器,定时检测特殊标记状态值。当状态值为“1”时,表明登录成功。

Controller:

1
2
3
4
5
6
7
8
9
/**
    * 检查二维码:登录页面轮询调用此方法,根据唯一标识code判断用户登录情况
    */
   @RequestMapping(value = "/qrcode/{code}", method = RequestMethod.GET)
   public Result qrcodeCeck(@PathVariable(name = "code") String code) throws Exception
{
       FaceLoginResult codeCheck = faceLoginService.checkQRCode(code);
       return new Result(ResultCode.SUCCESS, codeCheck);
  }

Service:

1
2
3
4
5
6
public FaceLoginResult checkQRCode(String code) {
       String cacheKey = getCacheKey(code);
       FaceLoginResult result = (FaceLoginResult)
redisTemplate.opsForValue().get(cacheKey);
       return result;
  }

( 4 )人脸检测/人脸登录:当用户扫码进入落地页,通过落地页打开摄像头,并且定时成像。将成像图片,通过接口提交给后端进行人脸检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 /**
    * 图像检测,判断图片中是否存在面部头像
    */
   @RequestMapping(value = "/checkFace", method = RequestMethod.POST)
   public Result checkFace(@RequestParam(name = "file") MultipartFile attachment)
throws Exception {
       if (attachment == null || attachment.isEmpty()) {
           throw new CommonException();
      }
       Boolean aBoolean =
baiduAiUtil.faceCheck(Base64Utils.encodeToString(attachment.getBytes()));
       if(aBoolean) {
           return new Result(ResultCode.SUCCESS);
      }else{
       return new Result(ResultCode.FAIL);
      }
  }

/**

1
2
3
4
5
6
7
8
* 检查二维码:登录页面轮询调用此方法,根据唯一标识code判断用户登录情况
*/
@RequestMapping(value = "/qrcode/{code}", method = RequestMethod.GET)
public Result qrcodeCeck(@PathVariable(name = "code") String code) throws Exception
{
FaceLoginResult codeCheck = faceLoginService.checkQRCode(code);
return new Result(ResultCode.SUCCESS, codeCheck);
}

( 5 )检测成功后,即进行人脸登录,人脸登录后,改变特殊标记状态值,成功为“1”,失败为“0”。当登录成功时,

进行自动登录操作,将token和userId存入到redis中。

Controller:

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping(value = "/{code}", method = RequestMethod.POST)
   public Result loginByFace(@PathVariable(name = "code") String code,
@RequestParam(name = "file") MultipartFile attachment) throws Exception {
       String userId = faceLoginService.loginByFace(code, attachment);
       if(userId == null) {
           return new Result(ResultCode.FAIL);
      }else{
           //构造返回数据
           return new Result(ResultCode.SUCCESS);
      }
  }

Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String loginByFace(String code, MultipartFile attachment) throws Exception {
       String userId =
baiduAiUtil.faceSearch(Base64Utils.encodeToString(attachment.getBytes()));
       FaceLoginResult result = new FaceLoginResult("1");
       if(userId != null) {
           User user = userDao.findById(userId).get();
           if(user != null) {
               Subject subject = SecurityUtils.getSubject();
               subject.login(new UsernamePasswordToken(user.getMobile(),
user.getPassword()));
               String token = subject.getSession().getId() + "";
               result = new FaceLoginResult("0",token,userId);
          }
      }
       redisTemplate.boundValueOps(getCacheKey(code)).set(result, 30,
TimeUnit.MINUTES);
       return userId;
  }

4.5.2 前端实现

前端主要实现的功能是,获取二维码并展示,然后后台轮询检测刷脸登录状态,并且实现落地页相关功能(摄像头调用、定时成像、发送人脸检测和发送人脸登录请求)

( 1 )二维码展现

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
// 二维码
   handlecode() {
     qrcode().then(res => {
     this.param.qrcode = res.data.file
       this.centerDialogVisible = true
       this.codeCheckInfo = res.data.code
       setInterval(() => {
         if (this.states === '-1') {
         codeCheck({ code: res.data.code }).then(res => {
           this.states = res.data.state
           this.token = res.data.token
           if (this.states === '0') {
           // 登录
           this.$store
            .dispatch('LoginByCode', res.data.token)
            .then(() => {
               this.$router.push({ path: '/' })
            })
            .catch(() => {
            })
          }
           if (this.states === '1') {
             // 关闭
             this.centerDialogVisible = false
          }
        })
      }
      }, 1000 * 10)
    })
  }

(2)落地页调用摄像头

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
handleClick() {
     let _this = this
     if (!this.vdstate) {
       return false
    }
     if (!_this.states) {
       // 注册拍照按钮的单击事件
     let video = this.$refs['vd']
     let canvas = this.$refs['cav']
     // let form = this.$refs["myForm"];
     let context = canvas.getContext('2d')
     // 绘制画面
     context.drawImage(video, 0, 0, 200, 200)
     let base64Data = canvas.toDataURL('image/jpg')
     // 封装blob对象
     let blob = this.dataURItoBlob(base64Data, 'camera.jpg') // base64 转图片file
     let formData = new FormData()
     formData.append('file', blob)
     this.imgUrl = base64Data
     checkFace(formData).then(res => {
       if (res.data.isSuc) {
         axios({
           method: 'post',
           url: '/api/frame/facelogin/' + this.$route.query.code,
           data: formData
        })
          .then(function(response) {
             console.log(response)
             _this.states = true
             _this.canvasShow = false
             _this.tipShow = true
             // _this.$message.success('验证通过' + '!')
          })
          .catch(function(error) {
             console.log(error)
          })
      } else {
         return false
      }
    })
    }
  },
   dataURItoBlob(base64Data) {
     var byteString
     if (base64Data.split(',')[0].indexOf('base64') >= 0)
       byteString = atob(base64Data.split(',')[1])
     else byteString = unescape(base64Data.split(',')[1])
     var mimeString = base64Data
      .split(',')[0]
      .split(':')[1]
      .split(';')[0]
     var ia = new Uint8Array(byteString.length)
     for (var i = 0; i < byteString.length; i++) {
       ia[i] = byteString.charCodeAt(i)
    }
     return new Blob([ia], { type: mimeString })
  }
}

4.6 总结

通过上述的步骤,可以实现一个刷脸登录的功能,其核心在于百度云AI的使用。通过合理的使用百度云AI SDK提供的相关API,我们可以很轻松的实现刷脸登录功能。刷脸登录的业务流程有很多种,我们只是实现了一种借助二维码的方式,作为抛砖引玉。更多的流程和实现方式,在此不进行赘述。

第 12 章 代码生成器原理分析

  • 理解代码生成器的需求和实现思路
  • 掌握freemaker的使用
  • 理解数据库中的元数据
  • 完成环境搭建工作

1 浅谈代码生成器

1.1 概述

在项目开发过程中,关注点更多是在业务功能的开发及保证业务流程的正确性上,对于重复性的代码编写占据了程序员大量的时间和精力,而这些代码往往都是具有规律的。就如controller、service、serviceImpl、dao、daoImpl、model、jsp的结构,用户、角色、权限等等模块都有类似的结构。针对这部分代码,就可以使用代码生成器,让计算机自动帮我们生成代码,将我们的双手解脱出来,减小了手工的重复劳动。

传统的方式程序员进行模块开发步骤如下:

  • 创建数据库表
  • 根据表字段设计实体类
  • 编写增删改查dao
  • 根据业务写service层
  • web层代码和前台页面

通常只需要知道了一个表的结构,增删改查的前后台页面的代码格式就是固定的,剩下的就是复杂的业务。而代码

生成工具的目标就是自动生成那部分固定格式的增删改查的代码

1.2 需求分析

image-20220922114515939

再我们的代码生成器中就是根据公共的模板和数据库表中的信息自动的生成代码。

  • 对于不借助代码生成工具的开发,程序员通常都是以一份已经写好的代码为基础进行代码Copy和修改,根据不同业务数据库表完善需求,可以将这份代码称之为公共的代码模板。
  • 生成的代码和数据库表中信息息息相关,所以除了模板之外还需要数据库表信息作为数据填充模板内容

1.3 实现思路

image-20220922114530295

代码生成器的实现有很多种,我们以从mysql数据库表结构生成对应代码为例来说明如何实现一个代码生成器。有以下几个重点:

1.数据库和表解析,用于生成model及其他代码

通过数据库解析获取数据库中表的名称、表字段等属性:可以根据表名称确定实体类名称,根据字段确定实体类中属性(如:tb_user表对应的实体类就是User)

2.模板开发生成代码文件

模板中定义公共的基础代码和需要替换的占位符内容(如:${tableName}最终会根据数据库表替换为User),根据解析好的数据库信息进行数据替换并生成代码文件

3 .基础框架的模板代码抽取

通过思路分析不难发现,对于代码生成工具而言只需要搞定数据库解析和模板开发。那么代码自动生成也并没有那么神秘和复杂。那接下来的课程和各位着重从这两个方面开始讲解直至完成属于自己的代码生成器。

2 深入FreeMarker

2.1 什么是FreeMarker

FreeMarker 是一款模板引擎:一种基于模板的、用来生成输出文本(任何来自于 HTML格式的文本用来自动生成源代码)的通用工具。它是为 Java 程序员提供的一个开发包或者说是类库。它不是面向最终用户,而是为程序员提供的可以嵌入他们开发产品的一款应用程序。

FreeMarker 的设计实际上是被用来生成 HTML 网页,尤其是通过基于实现了 MVC(ModelView Controller,模型-视图-控制器)模式的 Servlet 应用程序。使用 MVC 模式的动态网页的构思使得你可以将前端设计者(编写 HTML)从程序员中分离出来。所有人各司其职,发挥其擅长的一面。网页设计师可以改写页面的显示效果而不受程序员编译代码的影响,因为应用程序的逻辑(Java 程序)和页面设计(FreeMarker 模板)已经分开了。页面模板代码不会受到复杂的程序代码影响。这种分离的思想即便对一个程序员和页面设计师是同一个人的项目来说都是非常有用的,因为分离使得代码保持简洁而且便于维护。

尽管 FreeMarker 也有编程能力,但它也不是像 PHP 那样的一种全面的编程语言。反而,Java 程序准备的数据来显示(比如 SQL 查询),FreeMarker 仅仅使用模板生成文本页面来呈现已经准备好的数据

image-20220922114616600

FreeMarker 不是 Web 应用框架。它是 Web 应用框架中的一个适用的组件,但是FreeMarker 引擎本身并不知道HTTP 协议或 Servlet。它仅仅来生成文本。即便这样,它也非常适用于非 Web 应用环境的开发

2.2 Freemarker的应用场景

( 1 )动态页面

基于模板配置和表达式生成页面文件,可以像jsp一样被客户端访问

( 2 )页面静态化

对于系统中频繁使用数据库进行查询但是内容更新很小的应用,都可以用FreeMarker将网页静态化,这样就避免

了大量的数据库访问请求,从而提高网站的性能

( 3 )代码生成器

可以自动根据后台配置生成页面或者代码

freemarker的特征与亮点

  • 强大的模板语言:有条件的块,迭代,赋值,字符串和算术运算和格式化,宏和函数,编码等更多的功能;

  • 多用途且轻量:零依赖,输出任何格式,可以从任何地方加载模板(可插拔),配置选项丰富;

  • 智能的国际化和本地化:对区域设置和日期/时间格式敏感。

  • XML处理功能:将dom-s放入到XML数据模型并遍历它们,甚至处理他们的声明

  • 通用的数据模型:通过可插拔适配器将java对象暴露于模板作为变量树。

2.3 Freemarker的基本使用

2.3.1 构造环境

创建maven工程codeutil,并引入响应坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<!--freemarker核心包 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.20</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
   </dependencies>

2.3.2 入门案例

( 1 )创建模板template.ftl

1
欢迎您:${username}

( 2 )使用freemarker完成操作

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
public class FreemarkTest01 {
   @Test
   public void testProcessTemplate() throws Exception {
//1.创建freeMarker配置实例
Configuration cfg = new Configuration();
//2.设置模板加载器:开始加载模板,并且把模板加载在缓存中
cfg.setTemplateLoader(new FileTemplateLoader(new File("templates")));
//3.创建数据模型
Map<String,Object> dataModel = new HashMap<>();
dataModel.put("username","张三");
//4.获取模板
Template template = cfg.getTemplate("temp01.ftl");
//
/**
* 5.处理模板内容(i.输出到文件)
* process:
* 参数一:数据模型(map集合)
* 参数二:Writer对象(文件,控制台)
*/
//i.输出到文件
//template.process(dataModel, new FileWriter(new
File("C:\\Users\\ThinkPad\\Desktop\\ihrm\\day12\\测试\\aa.text")));
//i.打印到控制台
template.process(dataModel, new PrintWriter(System.out));//在控制台输出内容
  }
 }

2.3.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
public class FreemarkTest02 {
   private Configuration conf;
   @Before
   public void init() {
conf = new Configuration();
  }
   @Test
   public void testProcessTemplateString() throws Exception {
String templateString = "欢迎您:${username}";
Map<String,Object> dataMap = new HashMap();
dataMap.put("username","张三");
StringWriter out = new StringWriter();
/**
* 自定义模板
* 1.模板名称
* 2.模板的正文内容
* 3.configuration对象
*/
Template template = new Template("templateString...",new
StringReader(templateString),conf);
//处理模板内容
template.process(dataMap, out);
System.out.println(out.toString());
  }
}

2.4 Freemarker模板

2.4.1 概述

FreeMarker模板文件主要有 5 个部分组成:

1.数据模型:模板能用的所有数据

2.文本,直接输出的部分

3.注释,即<#–…–>格式不会输出

4.插值(Interpolation):即${..}或者#{..}格式的部分,将使用数据模型中的部分替代输出

  1. FTL指令:FreeMarker指令,和HTML标记类似,名字前加#予以区分,不会输出。

2.4.2 数据模型

FreeMarker(还有模板开发者)并不关心数据是如何计算的,FreeMarker 只是知道真实的数据是什么。模板能用的所有数据被包装成 data-model 数据模型

image-20220922114730149

2.4.3 模板的常用标签

在FreeMarker模板中可以包括下面几个特定部分:

  1. ${…}:称为interpolations,FreeMarker会在输出时用实际值进行替代。
  • ${name}可以取得root中key为name的value。
  • ${person.name}可以取得成员变量为person的name属性
  1. <#…>:FTL标记(FreeMarker模板语言标记):类似于HTML标记,为了与HTML标记区分

  2. <@>:宏,自定义标签

  3. 注释:包含在<#–和–>(而不是)之间

2.4.4 模板的常用指令

if指令

分支控制语句

1
2
3
4
5
6
7
8
9
<#if condition>
    ....
     <#elseif condition2>
    ...
     <#elseif condition3>      
    ...
     <#else>
    ...
     </#if>

list、break指令

list指令时一个典型的迭代输出指令,用于迭代输出数据模型中的集合

1
2
3
4
5
6
7
8
9
10
11
<#list sequence as item>
        ...
      </#list>
  除此之外,迭代集合对象时,还包括两个特殊的循环变量:
      a、item_index:当前变量的索引值。
      b、item_has_next:是否存在下一个对象
      也可以使用<#break>指令跳出迭代
       <#list ["星期一","星期二","星期三","星期四","星期五"] as x>
          ${x_index +1}.${x} <#if x_has_next>,</#if>
           <#if x = "星期四"><#break></#if>
       </#list>

include 指令

include指令的作用类似于JSP的包含指令,用于包含指定页,include指令的语法格式如下

1
2
3
4
5
6
<#include filename [options]></#include>
          在上面的语法格式中,两个参数的解释如下
          a、filename:该参数指定被包含的模板文件
          b、options:该参数可以省略,指定包含时的选项,包含encoding和parse两个选项,encoding
  指定包含页面时所使用的解码集,而parse指定被
          包含是否作为FTL文件来解析。如果省略了parse选项值,则该选项值默认是true

assign指令

它用于为该模板页面创建或替换一个顶层变量

1
<#assign name = zhangsan />

内置函数

FreeMarker还提供了一些内建函数来转换输出,可以在任何变量后紧跟?,?后紧跟内建函数,就可通过内建函数来转换输出变量。下面是常用的内建的字符串函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
?html:html字符转义
 
        ?cap_first: 字符串的第一个字母变为大写形式
 
        ?lower_case :字符串的小写形式
 
        ?upper_case :字符串的大写形式
 
        ?trim:去掉字符串首尾的空格
 
        ?substring:截字符串
 
        ?lenth: 取长度
 
        ?size: 序列中元素的个数
 
        ?int : 数字的整数部分(比如- 1.9?int 就是- 1)
          ?replace:字符串替换

3 数据库之元数据

3.1 数据库中的元数据

( 1 ) 什么是数据元数据?

元数据(MetaData),是指定义数据结构的数据。那么数据库元数据就是指定义数据库各类对象结构的数据。 例如数据库中的数据库名,表明, 列名、用户名、版本名以及从SQL语句得到的结果中的大部分字符串是元数据

( 2 )数据库元数据的作用

在应用设计时能够充分地利用数据库元数据

深入理解了数据库组织结构,再去理解数据访问相关框架的实现原理会更加容易。

( 3 )如何获取元数据

在我们前面使用JDBC来处理数据库的接口主要有三个,即Connection,PreparedStatement和ResultSet这三个,而对于这三个接口,还可以获取不同类型的元数据,通过这些元数据类获得一些数据库的信息。下面将对这三种类型的元数据对象进行各自的介绍并通过使用MYSQL数据库进行案例说明(部分代码再不同数据库中略有不同,学员如有其他需求请查阅API)

3.2 数据库元数据

3.2.1 概述

数据库元数据(DatabaseMetaData):是由Connection对象通过getMetaData方法获取而来,主要封装了是对

数据库本身的一些整体综合信息,例如数据库的产品名称,数据库的版本号,数据库的URL,是否支持事务等等。

以下有一些关于DatabaseMetaData的常用方法:

  • getDatabaseProductName:获取数据库的产品名称
  • getDatabaseProductName:获取数据库的版本号
  • getUserName:获取数据库的用户名
  • getURL:获取数据库连接的URL
  • getDriverName:获取数据库的驱动名称
  • driverVersion:获取数据库的驱动版本号
  • isReadOnly:查看数据库是否只允许读操作
  • supportsTransactions:查看数据库是否支持事务

3.2.2 入门案例

( 1 )构建环境

1
2
3
4
5
<dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <version>5.1.6</version>
       </dependency>

( 2 )获取数据库综合信息

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
public class DataBaseMetaDataTest {
   private Connection conn;
   @Before
   public void init() throws Exception {
       Class.forName("com.mysql.jdbc.Driver");
       Properties props =new Properties();
       //设置连接属性,使得可获取到表的REMARK(备注)
       props.put("remarksReporting","true");
       props.put("user", "root");
       props.put("password", "111111");
       conn = java.sql.DriverManager.
               getConnection("jdbc:mysql://127.0.0.1:3306/?
useUnicode=true&amp;characterEncoding=UTF8", props);
  }
   @Test
   public void testDatabaseMetaData() throws SQLException {
       //获取数据库元数据
       DatabaseMetaData dbMetaData =  conn.getMetaData();
       //获取数据库产品名称
       String productName = dbMetaData.getDatabaseProductName();
       System.out.println(productName);
       //获取数据库版本号
       String productVersion = dbMetaData.getDatabaseProductVersion();
       System.out.println(productVersion);
       //获取数据库用户名
       String userName = dbMetaData.getUserName();
       System.out.println(userName);
       //获取数据库连接URL
       String userUrl = dbMetaData.getURL();
       System.out.println(userUrl);
       //获取数据库驱动
       String driverName = dbMetaData.getDriverName();
       System.out.println(driverName);
       //获取数据库驱动版本号
       String driverVersion = dbMetaData.getDriverVersion();
       System.out.println(driverVersion);
       //查看数据库是否允许读操作
       boolean isReadOnly = dbMetaData.isReadOnly();
       System.out.println(isReadOnly);
       //查看数据库是否支持事务操作
       boolean supportsTransactions = dbMetaData.supportsTransactions();
       System.out.println(supportsTransactions);
  }
}

( 3 ) 获取数据库列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
   public void testFindAllCatalogs() throws Exception {
       //获取元数据
       DatabaseMetaData metaData = conn.getMetaData();
       //获取数据库列表
       ResultSet rs = metaData.getCatalogs();
       //遍历获取所有数据库表
       while(rs.next()){
           //打印数据库名称
           System.out.println(rs.getString(1));
      }
       //释放资源
       rs.close();
       conn.close();
  }

( 4 ) 获取某数据库中的所有表信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 @Test
   public void testFindAllTable() throws Exception{
       //获取元数据
       DatabaseMetaData metaData = conn.getMetaData();
       //获取所有的数据库表信息
       ResultSet tablers = metaData.getTables("ihrm", "", "bs_user", new String[]
{"TABLE"});
       //拼装table
       while(tablers.next()) {
           //所属数据库
           System.out.println(tablers.getString(1));
           //所属schema
           System.out.println(tablers.getString(2));
           //表名
           System.out.println(tablers.getString(3));
           //数据库表类型
           System.out.println(tablers.getString(4));
           //数据库表备注
           System.out.println(tablers.getString(5));
      }
  }

3.3 参数元数据

参数元数据(ParameterMetaData):是由PreparedStatement对象通过getParameterMetaData方法获取而来,主要是针对PreparedStatement对象和其预编译的SQL命令语句提供一些信息,ParameterMetaData能提供占位符参数的个数,获取指定位置占位符的SQL类型等等

以下有一些关于ParameterMetaData的常用方法:

  • getParameterCount:获取预编译SQL语句中占位符参数的个数
1
2
3
4
5
6
7
8
9
10
11
 @Test
   public void test() throws Exception {
       String sql = "select * from bs_user where id=?";
       PreparedStatement pstmt = conn.prepareStatement(sql);
       pstmt.setString(1, "1063705482939731968");
       //获取ParameterMetaData对象
       ParameterMetaData paramMetaData = pstmt.getParameterMetaData();
       //获取参数个数
       int paramCount = paramMetaData.getParameterCount();
       System.out.println(paramCount);
  }

3.4 结果集元数据

结果集元数据(ResultSetMetaData):是由ResultSet对象通过getMetaData方法获取而来,主要是针对由数据库执行的SQL脚本命令获取的结果集对象ResultSet中提供的一些信息,比如结果集中的列数、指定列的名称、指定列的SQL类型等等,可以说这个是对于框架来说非常重要的一个对象。

以下有一些关于ResultSetMetaData的常用方法:

  • getColumnCount:获取结果集中列项目的个数
  • getColumnType:获取指定列的SQL类型对应于Java中Types类的字段
  • getColumnTypeName:获取指定列的SQL类型
  • getClassName:获取指定列SQL类型对应于Java中的类型(包名加类名)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 @Test
   public void test() throws Exception {
       String sql = "select * from bs_user where id=?";
       PreparedStatement pstmt = conn.prepareStatement(sql);
       pstmt.setString(1, "1063705482939731968");
       //执行sql语句
       ResultSet rs = pstmt.executeQuery() ;
       //获取ResultSetMetaData对象
       ResultSetMetaData metaData = rs.getMetaData();
       //获取查询字段数量
       int columnCount = metaData.getColumnCount() ;
       for (int i=1;i<=columnCount;i++) {
           //获取表名称
           String columnName = metaData.getColumnName(i);
           //获取java类型
           String columnClassName = metaData.getColumnClassName(i);
           //获取sql类型
           String columnTypeName = metaData.getColumnTypeName(i);
           System.out.println(columnName);
           System.out.println(columnClassName);
           System.out.println(columnTypeName);
      }
       System.out.println(columnCount);
  }

4 代码生成器搭建环境

4.1 思路分析

工具的执行逻辑如下图所示:

image-20220922115036712

如上分析,得知完成代码生成器需要以下几个操作:

  1. 用户填写的数据库信息,工程搭建信息需要构造到实体类对象中方便操作

  2. 数据库表信息,数据库字段信息需要构造到实体类中

  3. 构造Freemarker数据模型,将数据库表对象和基本配置存入到Map集合中

  4. 借助Freemarker完成代码生成

  5. 自定义公共代码模板

4.2 搭建环境

4.2.1 配置坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencies>
       <dependency>
           <groupId>org.freemarker</groupId>
           <artifactId>freemarker</artifactId>
           <version>2.3.20</version>
       </dependency>
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <version>4.12</version>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <version>5.1.6</version>
             </dependency>
   </dependencies>

4.2.2 配置实体类

( 1 ) UI页面获取的数据库配置,封装到数据库实体类中

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
//数据库实体类
public class DataBase {
   private static String mysqlUrl = "jdbc:mysql://[ip]:[port]/[db]?
useUnicode=true&amp;characterEncoding=UTF8";
   private static String oracleUrl = "jdbc:oracle:thin:@[ip]:[port]:[db]";
   private String dbType;//数据库类型
   private String driver;
   private String userName;
   private String passWord;
   private String url;
   public DataBase() {}
   public DataBase(String dbType) {
       this(dbType,"127.0.0.1","3306","");
  }
   public DataBase(String dbType,String db) {
       this(dbType,"127.0.0.1","3306",db);
  }
   public DataBase(String dbType,String ip,String port,String db) {
       this.dbType = dbType;
       if("MYSQL".endsWith(dbType.toUpperCase())) {
           this.driver="com.mysql.jdbc.Driver";
           this.url=mysqlUrl.replace("[ip]",ip).replace("[port]",port).replace("
[db]",db);
      }else{
           this.driver="oracle.jdbc.driver.OracleDriver";
           this.url=oracleUrl.replace("[ip]",ip).replace("[port]",port).replace("
[db]",db);
      }
  }
   public String getDbType() {
       return dbType;
  }
   public void setDbType(String dbType) {
       this.dbType = dbType;
  }
   public String getDriver() {
       return driver;
       }
   public void setDriver(String driver) {
       this.driver = driver;
  }
   public String getUserName() {
       return userName;
  }
   public void setUserName(String userName) {
       this.userName = userName;
  }
   public String getPassWord() {
       return passWord;
  }
   public void setPassWord(String passWord) {
       this.passWord = passWord;
  }
   public String getUrl() {
       return url;
  }
   public void setUrl(String url) {
       this.url = url;
  }
}

( 2 ) UI页面获取的自动生成工程配置,封装到设置实体类中

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
public class Settings {
   private String project="example";
   private String pPackage="com.example.demo";
   private String projectComment;
   private String author;
   private String path1="com";
   private String path2="example";
   private String path3="demo";
   private String pathAll;
   public Settings(String project, String pPackage, String projectComment, String
author) {
       if(StringHelper.isNotBlank(project)) {
           this.project = project;
      }
       if(StringHelper.isNotBlank(pPackage)) {
           this.pPackage = pPackage;
      }
       this.projectComment = projectComment;
       this.author = author;
       String[] paths = pPackage.split("\\.");
       path1 = paths[0];
       path2 = paths.length>1?paths[1]:path2;
       path3 = paths.length>2?paths[2]:path3;
       pathAll = pPackage.replaceAll(".","/");
  }
   public Map<String, Object> getSettingMap(){
       Map<String, Object> map = new HashMap<>();
       Field[] declaredFields = Settings.class.getDeclaredFields();
       for (Field field : declaredFields) {
           field.setAccessible(true);
           try{
               map.put(field.getName(), field.get(this));
          }catch (Exception e){}
      }
       return map;
  }
   public String getProject() {
       return project;
  }
   public void setProject(String project) {
       this.project = project;
  }
   public String getpPackage() {
       return pPackage;
  }
   public void setpPackage(String pPackage) {
       this.pPackage = pPackage;
  }
   public String getProjectComment() {
       return projectComment;
  }
   public void setProjectComment(String projectComment) {
       this.projectComment = projectComment;
  }
   public String getAuthor() {
       return author;
  }
   public void setAuthor(String author) {
       this.author = author;
  }
   public String getPath1() {
    return path1;
  }
   public void setPath1(String path1) {
       this.path1 = path1;
  }
   public String getPath2() {
       return path2;
  }
   public void setPath2(String path2) {
       this.path2 = path2;
  }
   public String getPath3() {
       return path3;
  }
   public void setPath3(String path3) {
       this.path3 = path3;
  }
   public String getPathAll() {
       return pathAll;
  }
   public void setPathAll(String pathAll) {
       this.pathAll = pathAll;
  }
}

( 3 ) 将查询数据表的元数据封装到Table实体类

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
public class Table {

private String name;//表名称
private String name2;//处理后的表名称
private String comment;//介绍
private String key;// 主键列
private List<Column> columns;//列集合
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getName2() {
return name2;
}
public void setName2(String name2) {
this.name2 = name2;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public List<Column> getColumns() {
return columns;
}
public void setColumns(List<Column> columns) {
this.columns = columns;
}
}

( 4 )将查询数据字段的元数据封装到Column实体类

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
/**
* 列对象
*/
public class Column {

private String columnName;//列名称
private String columnName2;//列名称(处理后的列名称)
private String columnType;//列类型
private String columnDbType;//列数据库类型
private String columnComment;//列备注D
private String columnKey;//是否是主键
public String getColumnName() {
return columnName;
}
public void setColumnName(String columnName) {
this.columnName = columnName;
}
public String getColumnName2() {
return columnName2;
}
public void setColumnName2(String columnName2) {
this.columnName2 = columnName2;
}
public String getColumnType() {
return columnType;
}
public void setColumnType(String columnType) {
this.columnType = columnType;
}
public String getColumnDbType() {
return columnDbType;
}
public void setColumnDbType(String columnDbType) {
this.columnDbType = columnDbType;
}
public String getColumnComment() {
return columnComment;
}
public void setColumnComment(String columnComment) {
this.columnComment = columnComment;
}
public String getColumnKey() {
return columnKey;
}
public void setColumnKey(String columnKey) {
this.columnKey = columnKey;
}
}

4.2.3 导入工具类

导入资料中提供的工具类

image-20220922121806648

FileUtils:文件处理工具类:

  • getRelativePath:获取文件的相对路径
  • searchAllFile :查询文件夹下所有文件
  • mkdir :创建文件目录

PropertiesMaps:加载所有properties并存入Map集合中:

  • 加载代码生成器的基本配置文件
  • 加载用户的自定义配置文件
  • 所有配置文件需要放置到”/properties”文件夹下

StringHelper:字符串处理工具类

4.2.4 *配置UI界面

为了方便演示,使用swing程序构造了一套UI页面,对于swing不需要大家掌握,直接使用即可

第 13 章 代码生成器实现

  • 实现封装元数据的工具类
  • 实现代码生成器的代码编写
  • 掌握模板创建的

1 构造数据模型

1.1 需求分析

借助Freemarker机制可以方便的根据模板生成文件,同时也是组成代码生成器的核心部分。对于Freemarker而言,其强调 数据模型 + 模板 = 文件 的思想,所以代码生成器最重要的一个部分之一就是数据模型。在这里数据

模型共有两种形式组成:

  • 数据库中表、字段等信息

针对这部分内容,可以使用元数据读取并封装到java实体类中

  • 用户自定义的数据

为了代码生成器匹配多样的使用环境,可以让用户自定义的数据,并且以key-value的形式配置到properties文件中

接下来我们一起针对这两方面的数据进行处理

1.2 自定义数据

通过PropertiesUtils工具类,统一对properties文件夹下的所有.properties文件进行加载,并存入内存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 需要将自定义的配置信息写入到properties文件中
* 配置到相对于工程的properties文件夹下
*/
public class PropertiesUtils {
   public static Map<String,String> customMap = new HashMap<>();
   static {
File dir = new File("properties");
try {
List<File> files = FileUtils.searchAllFile(new
File(dir.getAbsolutePath()));
for (File file : files) {
if(file.getName().endsWith(".properties")) {
Properties prop = new Properties();
prop.load(new FileInputStream(file));
customMap.putAll((Map) prop);
} }
} catch (IOException e) {
           e.printStackTrace();
      }
  }
}

1.3 元数据处理

加载指定数据库表,将表信息转化为实体类对象(Table)

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
 /**
    * 获取表及字段信息
    */
   public static List<Table> getDbInfo(DataBase db,String tableNamePattern) throws
Exception {
       //创建连接
       Connection connection = getConnection(db.getDriver(),db.getUserName(),
db.getPassWord(), db.getUrl());
       //获取元数据
       DatabaseMetaData metaData = connection.getMetaData();
       //获取所有的数据库表信息
       ResultSet tablers = metaData.getTables(null, null, tableNamePattern, new
String[]{"TABLE"});
       List<Table> list=new ArrayList<Table>();
       //拼装table
       while(tablers.next()) {
           Table table = new Table();
           String tableName=tablers.getString("TABLE_NAME");
           //如果为垃圾表
           if(tableName.indexOf("=")>=0 || tableName.indexOf("$")>=0){
               continue;
          }
           table.setName(tableName);
           table.setComment(tablers.getString("REMARKS"));
           //获得主键
           ResultSet primaryKeys = metaData.getPrimaryKeys(null, null, tableName);
           List<String> keys=new ArrayList<String>();
           while(primaryKeys.next()){
               String keyname=primaryKeys.getString("COLUMN_NAME");
               //判断 表名为全大写 ,则转换为小写
               if(keyname.toUpperCase().equals(keyname)){
                   keyname=keyname.toLowerCase();//转换为小写
              }
               keys.add(keyname);
          }
           //获得所有列
           ResultSet columnrs = metaData.getColumns(null, null, tableName, null);
           List<Column> columnList=new ArrayList<Column>();
           while(columnrs.next()){
               Column column=new Column();
               //处理字段
               String columnName=  columnrs.getString("COLUMN_NAME");
               //字段名称
               column.setColumnName(columnName);
               column.setColumnName2(StringUtils.toJavaVariableName(columnName));
               //字段类型
               String columnDbType = columnrs.getString("TYPE_NAME");
               column.setColumnDbType(columnDbType);//数据库原始类型
               //java类型
               Map<String, String> convertMap = PropertiesUtils.customMap;
               String typeName = convertMap.get(columnDbType);//获取转换后的类型
               if(typeName==null) {
                   typeName=columnrs.getString("TYPE_NAME");
              }
               column.setColumnType(typeName);
               String remarks = columnrs.getString("REMARKS");//备注
               column.setColumnComment(StringUtils.isBlank(remarks)?
columnName:remarks);
               //如果该列是主键
               if(keys.contains(columnName)){
                   column.setColumnKey("PRI");
                   table.setKey(column.getColumnName());
              }else {
                   column.setColumnKey("");
              }
               columnList.add(column);
          }
           columnrs.close();
           table.setColumns(columnList);
           list.add(table );
      }
       tablers.close();
       connection.close();
       return list;
  }

2 实现代码生成

2.1 需求分析

为了代码更加直观和易于调用,实现代码生成共有两个类组成:

  • UI界面统一调用的入口类:GeneratorFacade

方便多种界面调用,主要完成数据模型获取,调用核心代码处理类完成代码生成

  • 代码生成核心处理类:Generator

根据数据模型和模板文件路径,统一生成文件到指定的输出路径

2.2 模板生成

( 1 )配置统一调用入口类GeneratorFacade

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
/**
* 1.根据传入数据库信息构造数据
* 2.根据模板完成代码生成
*/
public class GeneratorFacade {
   private Generator generator;
   //公共数据Map集合(处理文件路径等公共代码替换)
   private Map<String,Object> commonMap;
   public GeneratorFacade(String templatePath, String outPath, Settings settings) {
       commonMap = settings.getSettingMap();
       commonMap.putAll(PropertiesUtils.customMap);
       try {
           generator = new Generator(templatePath,outPath);
      }catch (Exception e){
      }
  }
   //针对数据库表生成
   public void generatorByTable(DataBase db,String tableName) throws Exception {
       //查询数据库获取所有表信息
       List<Table> tableList = DataBaseUtils.getDbInfo(db, tableName);
       for (Table table : tableList) {
           //根据数据库表信息,构造数据模型并生成代码
           generator.scanTemplatesAndProcess(getTemplateModel(table));
      }
  }
  //根据数据库对象table构造数据模型
   private Map getTemplateModel(Table table) {
       Map<String,Object> templateMap = new HashMap();
       //table表信息
       templateMap.put("table",table);
       //实体类名称
       String prefixs = (String) commonMap.get("tableRemovePrefixes");
       String className = table.getName();
       for(String prefix : prefixs.split(",")) {
           className = StringUtils.removePrefix(className, prefix,true);
      }
       templateMap.put("ClassName",
StringUtils.makeAllWordFirstLetterUpperCase(className));
       
       //公共的配置和自定义配置
       templateMap.putAll(commonMap);
       return templateMap;
  }
}

( 2 )处理模板代码生成的核心类Generator

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
public class Generator {
   //模板所在路径
   private String templatePath;
   //代码生成路径
   private String outPath;
   private Configuration conf;
   public Generator(String templatePath, String outPath) throws Exception {
       this.templatePath = templatePath;
       this.outPath = outPath;
       //创建freemarker的核心配置类
       conf = new Configuration();
       //指定模板加载器
       conf.setTemplateLoader(new FileTemplateLoader(new File(templatePath)));
  }
   //扫描所有模板并进行代码生成
   public void scanTemplatesAndProcess(Map dataMap) throws Exception {
       //加载文件夹下的所有模板文件
       List<File> srcFiles = FileUtils.searchAllFile(new File(templatePath));
       //针对每一个模板文件进行代码生成
       for(File srcFile :srcFiles) {
           executeGenerate(dataMap, srcFile);
      }
  }
   //对某个模板生成代码
   private void executeGenerate(Map dataMap ,File srcFile) throws Exception {
       //获取文件路径
       String templateFile = srcFile.getAbsolutePath().replace(this.templatePath,"");
       //对文件名称进行处理(字符串替换)
       String outputFilepath = processTemplateString(templateFile,dataMap);
       //读取模板
       Template template = conf.getTemplate(templateFile);
       //设置字符集
       template.setOutputEncoding("encode");
       //创建文件
       File outFile = FileUtils.mkdir(outPath,outputFilepath);
       FileWriter fileWriter = new FileWriter(outFile);
       //模板生成
       template.process(dataMap,fileWriter);
       fileWriter.close();
  }
}

2.3 路径处理

使用字符串模板对文件生成路径进行统一处理

1
2
3
4
5
6
7
8
//处理字符串模板
   private String processTemplateString(String templateString,Map dataMap) throws
Exception {
       StringWriter out = new StringWriter();
       Template template = new Template("ts",new StringReader(templateString),conf);
       template.process(dataMap, out);
       return out.toString();
  }

3 制作模板

3.1 模板制作的约定

( 1 )模板位置

模板统一放置到相对于当前路径的模板文件夹下

image-20220922122243671

( 2 )自定义数据

image-20220922122321580

自定义的数据以.propeties文件(key-value)的形式存放入相对于当前路径的properties文件夹下

( 3 )数据格式

table中数据内容:

image-20220922122336452

3.2 需求分析

制作通用的SpringBoot程序的通用模板

  • 实体类

类路径,类名,属性列表(getter,setter方法)

  • 持久化层

类路径,类名,引用实体类

  • 业务逻辑层

类路径,类名,引用实体类,引用持久化层代码

  • 视图层

类路径,类名,引用实体类,引用业务逻辑层代码,请求路径

  • 配置文件

pom文件,springboot配置文件

3.3 SpringBoot通用模板

3.3.1 实体类

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
package ${pPackage}.pojo;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* ${comment!}服务层
* @author ${author!"itcast"}
*/
@Entity
@Table(name="${table.name}")
public class ${ClassName} implements Serializable {
//定义私有属性
<#list table.columns as column>
<#if column.columnKey??>
@Id
</#if>
private ${column.columnType} ${column.columnName2};
</#list>

//处理getter,setter方法
<#list table.columns as column>
public void set${column.columnName2?cap_first}(${column.columnType} value) {
this.${column.columnName2} = value;
}

public ${column.columnType} get${column.columnName2?cap_first}() {
return this.${column.columnName2};
}
</#list> }

3.3.2 持久化层

1
2
3
4
5
6
7
8
9
10
11
12
package ${pPackage}.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import ${pPackage}.pojo.${ClassName};
/**
* ${comment!}数据访问接口
* @author ${author!"itcast"}
*/
public interface ${ClassName}Dao extends
JpaRepository<${ClassName},String>,JpaSpecificationExecutor<${ClassName}>{

}

3.3.3 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
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
<#assign classNameLower = ClassName ? uncap_first>
package ${pPackage}.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import util.IdWorker;
import ${pPackage}.dao.${ClassName}Dao;
import ${pPackage}.pojo.${ClassName};
/**
* ${comment!}服务层
* @author ${author!"itcast"}
*/
@Service
public class ${ClassName}Service {
@Autowired
private ${ClassName}Dao ${classNameLower}Dao;

@Autowired
private IdWorker idWorker;
/**
* 查询全部列表
* @return
*/
public List<${ClassName}> findAll() {
return ${classNameLower}Dao.findAll();
}
/**
* 分页查询
*
* @param page
* @param size
* @return
*/
public Page<${ClassName}> findPage(int page, int size) {
PageRequest pageRequest = PageRequest.of(page-1, size);
return ${classNameLower}Dao.findAll(pageRequest);
}
/**
* 根据ID查询实体
* @param id
* @return
*/
public ${ClassName} findById(String id) {
return ${classNameLower}Dao.findById(id).get();
}
/**
* 增加
* @param ${ClassName}
*/
public void add(${ClassName} ${ClassName}) {
${ClassName}.setId( idWorker.nextId()+"" );
${classNameLower}Dao.save(${ClassName});
}
/**
* 修改
* @param ${ClassName}
*/
public void update(${ClassName} ${ClassName}) {
${classNameLower}Dao.save(${ClassName});
}
/**
* 删除
* @param id
*/
public void deleteById(String id) {
${classNameLower}Dao.deleteById(id);
}
}

3.3.4 Controller

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
<#assign classNameLower = ClassName ? uncap_first>
package ${pPackage}.controller;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import ${pPackage}.pojo.${ClassName};
import ${pPackage}.service.${ClassName}Service;
import entity.PageResult;
import entity.Result;
/**
* [comment]控制器层
* @author Administrator
*
*/
@RestController
@CrossOrigin
@RequestMapping("/${classNameLower}")
public class ${ClassName}Controller {
@Autowired
private ${ClassName}Service ${classNameLower}Service;


/**
* 查询全部数据
* @return
*/
@RequestMapping(method= RequestMethod.GET)
public Result findAll(){
return new Result(ResultCode.SUCCESS,${classNameLower}Service.findAll());
}

/**
* 根据ID查询
* @param id ID
* @return
*/
@RequestMapping(value="/{id}",method= RequestMethod.GET)
public Result findById(@PathVariable String id){
return new Result(ResultCode.SUCCESS,${classNameLower}Service.findById(id));
}
/**
* 分页查询全部数据
* @param page
* @param size
* @return
*/
@RequestMapping(value="/{page}/{size}",method=RequestMethod.GET)
public Result findPage(@PathVariable int page,@PathVariable int size){
Page<${ClassName}> searchPage = ${classNameLower}Service.findPage(page, size);
PageResult<Role> pr = new
PageResult(searchPage.getTotalElements(),searchPage.getContent());
return new Result(ResultCode.SUCCESS,pr);
}
/**
* 增加
* @param ${classNameLower}
*/
@RequestMapping(method=RequestMethod.POST)
public Result add(@RequestBody ${ClassName} ${classNameLower} ){
${classNameLower}Service.add(${classNameLower});
return new Result(ResultCode.SUCCESS);
}

/**
* 修改
* @param ${classNameLower}
*/
@RequestMapping(value="/{id}",method= RequestMethod.PUT)
public Result update(@RequestBody ${ClassName} ${classNameLower}, @PathVariable
String id ){
${classNameLower}.setId(id);
${classNameLower}Service.update(${classNameLower});
return new Result(ResultCode.SUCCESS);
}

/**
* 删除
* @param id
*/
@RequestMapping(value="/{id}",method= RequestMethod.DELETE)
public Result delete(@PathVariable String id ){
${classNameLower}Service.deleteById(id);
return new Result(ResultCode.SUCCESS);
}
}

3.3.5 配置文件

( 1 )application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9001
spring:
application:
name: ${project}-${path3} #指定服务名
datasource:
driverClassName: ${driverName}
url: ${url}
username: ${dbuser}
password: ${dbpassword}
jpa:
database: MySQL
show-sql: true

( 2 )pom.xml

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
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
   <groupId>${path_1}.${path2}</groupId>
   <artifactId>${project}_parent</artifactId>
   <version>0.0.1-SNAPSHOT</version>
 </parent>
 <artifactId>${project}_${path3}</artifactId>
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>${path1}.${path2}</groupId>
<artifactId>${project}_common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
 </dependencies>  
</project>