员工管理系统

需求分析

模块,功能,技术选型:

  • 用户模块:

    • 1、用户登录
      • 2、用户注册
      • 3、验证码实现
      • 4、欢迎xx用户登录
      • 5、安全退出
  • 员工管理模块:

    • 6、员工信息展示
    • 7、员工的添加
    • 8、员工的删除
    • 9、员工的修改
    • 10、员工列表加入Redis缓存实现
  • 技术选型

    • 前端:vue+axios
    • 后端:springboot + mybatis + mysql + redis

库表设计

1、系统需要哪些表

用户表

员工表

2、分析表与表之间的关系

用户表管理员工表,没有什么关系,涉及到的都是单表操作

3、分析表中的字段

用户表字段
id、username、realname、password、gender、status、registerTime

员工表

id、name、photoPath、salary、age

创建emp数据库,创建表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
create table t_user(
id int(6) primary key auto_increment,
username varchar(50),
realname varchar(50),
password varchar(50),
gender varchar(4),
status varchar(4),
regist_time timestamp
);

create table t_emp(
id int(6) primary key auto_increment,
name varchar(50),
photo_path varchar(100),
salary double(10,2),
age int(3)
)

详细设计

流程图,伪代码(小项目省略)

编码环节

环境准备

springboot+mybatis+mysql、引入员工系统页面

项目名:emps

项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
-src/main/java
--------------com.zyz.
---------------------bean
---------------------dao
---------------------service
---------------------controller
---------------------utils
---------------------cache
-src/main/resource
------------------application.yml
------------------com.zyz.mapper/ mapper配置文件
------------------com.zyz.sql/ 数据库文件
------------------static/ 静态资源

正式进入编码

一、用户模块

1、创建springboot项目,勾选相应的依赖

搭建项目基本结构:

引入其他依赖

1
2
3
4
5
6
7
8
9
10
 <dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>

2、编写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
# 指定工程访问路径
server:
servlet:
context-path: /empAdmin

spring:
datasource:
username: root
password: 2824199842
url: jdbc:mysql://127.0.0.1:3306/emp?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource

mybatis:
type-aliases-package: com.zyz.bean
mapper-locations: classpath:com/zyz/mapper/*.xml # 要在接口中添加@Mapper注解
configuration:
map-underscore-to-camel-case: true

logging:
level:
com:
zyz:
dao: debug
service: info
controller: info

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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package com.zyz.utils;

public class VerifyCodeUtils{
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();

/**
* 使用系统默认字符源生成验证码
* @param verifySize 验证码长度
* @return
*/
public static String generateVerifyCode(int verifySize){
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/**
* 使用指定源生成验证码
* @param verifySize 验证码长度
* @param sources 验证码字符源
* @return
*/
public static String generateVerifyCode(int verifySize, String sources){
if(sources == null || sources.length() == 0){
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for(int i = 0; i < verifySize; i++){
verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
}
return verifyCode.toString();
}

/**
* 生成随机验证码文件,并返回验证码值
* @param w
* @param h
* @param outputFile
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
}

/**
* 输出随机验证码图片流,并返回验证码值
* @param w
* @param h
* @param os
* @param verifySize
* @return
* @throws IOException
*/
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException{
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
}

/**
* 生成指定验证码图像文件
* @param w
* @param h
* @param outputFile
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, File outputFile, String code) throws IOException{
if(outputFile == null){
return;
}
File dir = outputFile.getParentFile();
if(!dir.exists()){
dir.mkdirs();
}
try{
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch(IOException e){
throw e;
}
}

/**
* 输出指定验证码图片流
* @param w
* @param h
* @param os
* @param code
* @throws IOException
*/
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
Color.PINK, Color.YELLOW };
float[] fractions = new float[colors.length];
for(int i = 0; i < colors.length; i++){
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);

g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);

Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h-4);

//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}

// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}

shear(g2, w, h, c);// 使图片扭曲

g2.setColor(getRandColor(100, 160));
int fontSize = h-4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for(int i = 0; i < verifySize; i++){
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
}

g2.dispose();
ImageIO.write(image, "jpg", os);
}

private static Color getRandColor(int fc, int bc) {
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}

private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}

private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}

private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}

private static void shearX(Graphics g, int w1, int h1, Color color) {

int period = random.nextInt(2);

boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);

for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}

}

private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period
+ (6.2831853071795862D * (double) phase)
/ (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}

}

}
}

创建UserController类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.zyz.controller;

@RestController
@CrossOrigin // 允许跨域
@RequestMapping("user")
public class UserController {
/**
* 生成验证码图片
* @return
*/
@GetMapping("getImageCode")
public String getImageCode(HttpServletRequest request) throws Exception {
// 使用工具类生成验证码并输出
ByteArrayOutputStream os = new ByteArrayOutputStream();
String code = VerifyCodeUtils.generateVerifyCode(4);
VerifyCodeUtils.outputImage(100, 30, os, code);
// 保存至applicationContext域中
request.getServletContext().setAttribute("code", code);
// 将图片转换为base64
String s = Base64Utils.encodeToString(os.toByteArray());
return "data:image/png;base64," + s;
}
}

在前端发起异步请求

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
<script src="js/vue.js"></script>
<script src="js/axios.min.js"></script>
<script>
var app = new Vue({
el: "#wrap",
data: {
url: "",
date: "",
},
methods: {
// 更换验证码
getImg() {
this.getSrc()
},
// 封装获取验证码的请求方法
getSrc() {
var _this = this;
axios.get("http://localhost:8080/empAdmin/user/getImageCode?time=" + Math.random())
.then(res => {
// console.log(res.data);
_this.url = res.data;
});
}
},
created() { // 页面加载前执行 发起异步请求
this.getSrc();
// 显示当前日期
this.date = new Date().getFullYear() + "/" + (new Date().getMonth() + 1) + "/" + new Date().getDate();
}
})
</script>
1
2
3
4
<td>
<img id="num" :src="url"/>
<a href="javascript:;" @click="getImg">换一张</a>
</td>
1
<p v-text="date"></p>

4、实现注册功能

创建User实体类,使用lombok!

1
2
3
4
5
6
7
8
9
10
11
package com.zyz.bean;
@Data
public class User {
private String id;
private String username;
private String realname;
private String password;
private String gender;
private String status;
private Date registerTime;
}

编写dao层

1
2
3
4
5
6
7
8
9
10
11
package com.zyz.dao;

@Mapper
@Repository
public interface UserDao {
// 添加用户
void save(User user);

// 根据用户名查找用户
User queryUserByName(String username);
}

对应的sql映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zyz.dao.UserDao">
<insert id="save" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into t_user
values
(#{id},#{username},#{realname},#{password},#{gender},#{status},#{registerTime})
</insert>

<select id="queryUserByName" parameterType="String" resultType="User">
select id,username,realname,password,gender,status,register_time
from t_user where username=#{username}
</select>
</mapper>

编写service层

1
2
3
4
5
6
package com.zyz.service;

public interface UserService {
// 用户注册
void register(User user);
}

实现对应的service接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.zyz.service.impl;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserDao userDao;
@Override
public void register(User user) {
// 判断用户是否存在
User user1 = userDao.queryUserByName(user.getUsername());
if(user1==null){
// 生成用户状态
user.setStatus("已激活");
// 设置用户注册时间
user.setRegisterTime(new Date());
// 调用dao
userDao.save(user);
}else{
throw new RuntimeException("用户名已存在!");
}
}
}

编写controller层,在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
28
29
30
31
32
33
@Autowired
private UserService userService;

@PostMapping("register") // @RequestBody将前端传来的json字符串转换为java对象
public Map<String, Object> register(@RequestBody User user, String code, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
try {
// 判断验证码
if (StringUtils.isEmpty(code)) {
throw new RuntimeException("验证码为空!");
}
String realCode = (String) request.getServletContext().getAttribute("code");
if (realCode.equalsIgnoreCase(code)) {
if (StringUtils.isEmpty(user.getUsername())) {
throw new RuntimeException("用户名为空!");
}
if (StringUtils.isEmpty(user.getPassword())) {
throw new RuntimeException("密码为空!");
}
// 调用service方法
userService.register(user);
map.put("status", true);
map.put("msg", "提示:注册成功!");
} else {
throw new RuntimeException("验证码错误!");
}
} catch (Exception e) {
e.printStackTrace();
map.put("status", false);
map.put("msg", "提示:" + e.getMessage());
}
return map;
}

编写前端页面

在vue对象的data中添加user对象,用来接收页面的参数

将页面中的表单项与user的每一个属性值绑定,.trim去掉前后空格

给按钮绑定注册事件

1
<input @click="register" type="button" class="button" value= " 注 册 "/>

发起注册请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注册
register() {
axios.post("http://localhost:8080/empAdmin/user/register?code=" + this.code, this.user)
.then(res => {
// console.log(res.data);
if (res.data.status) {
// 注册成功,跳转登录页面
alert(res.data.msg + "点击确定跳转至登录页面!");
location.href = "http://localhost:8080/empAdmin/login.html";
} else {
// 注册失败
alert(res.data.msg + "点击确定重新注册!");
}
});
}

5、实现登录功能

dao层

已经有通过名字查询用户信息的方法queryUserByName(String username),service层直接调用即可

service层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public User login(User user) {
if(StringUtils.isEmpty(user.getUsername())){
throw new RuntimeException("用户名为空!");
}
if(StringUtils.isEmpty(user.getPassword())){
throw new RuntimeException("密码为空!");
}
// 根据输入的用户名查询用户
User user1 = userDao.queryUserByName(user.getUsername());
// 判断用户是否存在 使用ObjectUtils!
if(!ObjectUtils.isEmpty(user1)){
if(user1.getPassword().equals(user.getPassword())){
return user1;
}else{
throw new RuntimeException("密码错误!");
}
}else{
throw new RuntimeException("用户名不存在或错误!");
}
}

controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@PostMapping("login")
public Map<String,Object> login(@RequestBody User user){
Map<String,Object> map = new HashMap<>();
try {
userService.login(user);
map.put("status",true);
map.put("msg","提示:登录成功!");
} catch (Exception e) {
e.printStackTrace();
map.put("status",false);
map.put("msg","提示:"+e.getMessage());
}
return map;
}

前端页面

1
<p v-text="date"></p>
1
<input type="text" v-model="user.username" />
1
<input type="password" v-model="user.password" />
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
<script>
var app = new Vue({
el: "#wrap",
data: {
date: "",
user: {},
},
created(){
this.date = new Date().getFullYear() + "/" + (new Date().getMonth() + 1) + "/" + new Date().getDate();
},
methods:{
login(){
// console.log(this.user);
axios.post("http://localhost:8080/empAdmin/user/login",this.user)
.then(res=>{
// console.log(res.data);
if(res.data.status){
alert(res.data.msg+"点击确定跳转员工管理页面!");
location.href="/empAdmin/emplist.html";
}else{
alert(res.data.msg+"点击确定重新登录!")
}
})
}
}
})
</script>

6、实现用户登录信息展示

登录成功后的信息都在后端这边,前端拿不到这些数据。

在前后端未分离的时候,通常都是通过服务端的HttpSession来保存这些信息,服务端在创建了Session的同时,会为该Session生成唯一的sessionId并保存到浏览器中,在随后的请求通过携带sessionId重新获得已经创建的Session;

而在前后端分离的系统中,前端与后端分别部署在不同的服务器上,前后端交互时,前端请求不会带上后端sessionId,session获取不到。因此将信息保存至浏览器中的localStorage中

在controller中保存用户信息:

登录成功后保存用户信息到localStorage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
login(){
// console.log(this.user);
axios.post("http://localhost:8080/empAdmin/user/login",this.user)
.then(res=>{
// console.log(res.data);
if(res.data.status){
alert(res.data.msg+"点击确定跳转员工管理页面!");
// 将用户信息存放指localStorage中
// JSON.stringify()将json对象转换为json字符串
localStorage.setItem("user",JSON.stringify(res.data.user));
location.href="/empAdmin/emplist.html";
}else{
alert(res.data.msg+"点击确定重新登录!")
}
})
}

在员工展示页面显示登录名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
var app = new Vue({
el: "#wrap",
data:{
date: "",
user:{} // 存放用户登录信息
},
created(){
this.date = new Date().getFullYear() + "/" + (new Date().getMonth() + 1) + "/" + new Date().getDate();
var userString = localStorage.getItem("user");
if(userString!=null){// 已登录
// 将json字符串转换为json对象
this.user = JSON.parse(userString);
}else{// 未登录
alert("请登录!点击确定跳转登录页面");
location.href="/empAdmin/login.html";
}
},
})
</script>
1
<p>用户:<span v-show="user!=null" v-text="user.username" style="color: red"></span>

7、实现退出功能

删除localStorage中的用户数据,跳转至登录页

1
2
3
4
5
6
7
methods:{
// 处理安全退出
logout(){
localStorage.removeItem("user");
location.href="/empAdmin/login.html";
}
}

二、员工模块

1、展示所有员工的信息

创建实体类

1
2
3
4
5
6
7
8
9
10
package com.zyz.bean;

@Data
public class Emp {
private String id;
private String name;
private String photoPath;
private Double salary;
private Integer age;
}

dao层

1
2
3
4
5
6
7
8
package com.zyz.dao;

@Mapper
@Repository
public interface EmpDao {
// 查询所有员工
List<Emp> listAll();
}
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zyz.dao.EmpDao">

<select id="listAll" resultType="Emp">
select id,name,photo_path,salary,age from t_emp
</select>

</mapper>

service层

1
2
3
4
5
6
package com.zyz.service;

public interface EmpService {
// 展示所有员工
List<Emp> listAll();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.zyz.service.impl;

@Service
public class EmpServiceImpl implements EmpService {

@Autowired
private EmpDao empDao;

@Override
public List<Emp> listAll() {
return empDao.listAll();
}
}

controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.zyz.controller;

@RestController
@RequestMapping("emp")
public class EmpController {
@Autowired
private EmpService empService;

@GetMapping("listAll")
public List<Emp> listAll(){
List<Emp> emps = empService.listAll();
return emps;
}
}

前端

在data中添加一个数组,用来存放所有员工

1
2
3
4
5
6
7
// 查询员工
axios.get("http://localhost:8080/empAdmin/emp/listAll")
.then(res=>{
// console.log(res.data);
this.emps = res.data;
// console.log(this.emps);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--遍历员工列表-->
<tr v-for="(emp,index) in emps" :key="emp.id" :class="index%2==0?'row1':'row2'">
<td v-text="emp.id"></td>
<td v-text="emp.name"></td>
<td>
<img :src="emp.photoPath" style="height: 60px;">
</td>
<td v-text="emp.salary"></td>
<td v-text="emp.age"></td>
<td>
<a href="emplist.html">删除</a>&nbsp;
<a href="updateEmp.html">修改</a>
</td>
</tr>

2、添加员工

dao层

添加添加员工的方法:

1
2
// 添加员工
void add(Emp emp);

对应的mapper文件

1
2
3
<insert id="add" parameterType="Emp" useGeneratedKeys="true" keyProperty="id">
insert into t_emp values(#{id},#{name},#{photoPath},#{salary},#{age})
</insert>

service层

1
void add(Emp emp);
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void add(Emp emp) {
if(StringUtils.isEmpty(emp.getName())){
throw new RuntimeException("请输入用户名!");
}
if(StringUtils.isEmpty(emp.getSalary())){
throw new RuntimeException("请输入薪资!");
}
if(StringUtils.isEmpty(emp.getAge())){
throw new RuntimeException("请输入年龄!");
}
empDao.add(emp);
}

controller层

1
2
3
4
# 配置上传文件的地址
upload:
dir:
D:\IDEA_workspace\emps\src\main\resources\static\photos
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
// 注入上传用户头像的地址 
@Value("${upload.dir}")
private String realPath;

@PostMapping("addEmp")
public Map<String, Object> addEmp(Emp emp, MultipartFile photo) throws IOException {
// System.out.println("员工信息:"+emp);
// System.out.println("头像:"+photo);
HashMap<String, Object> map = new HashMap<>();
try {
// 头像保存
// 1、修改文件名
if (!ObjectUtils.isEmpty(photo)) {
String newFileName = UUID.randomUUID().toString() + "." +
FilenameUtils.getExtension(photo.getOriginalFilename());
// System.out.println(newFileName);
// 2、图像上传
photo.transferTo(new File(realPath, newFileName));
// 3、设置头像访问地址
emp.setPhotoPath(newFileName);
// 添加员工到数据库中
empService.add(emp);
map.put("status", true);
map.put("msg", "添加成功!");
} else {
throw new RuntimeException("请上传头像");
}

} catch (Exception e) {
e.printStackTrace();
map.put("status", false);
map.put("msg", "提示:" + e.getMessage());
}
return map;
}

前端

绑定表单属性,对文件添加引用

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
<script>
var app = new Vue({
el: "#wrap",
data:{
date: "",
user:{}, // 存放用户登录信息
emp:{ // 存放员工信息
name:"",
age:"",
salary:"",
empPhoto:""
},
},
created(){
this.date = new Date().getFullYear() + "/" + (new Date().getMonth() + 1) + "/" + new Date().getDate();
var userString = localStorage.getItem("user");
if(userString!=null){// 已登录
// 将json字符串转换为json对象
this.user = JSON.parse(userString);
}else{// 未登录
alert("请登录!点击确定跳转登录页面");
location.href="/empAdmin/login.html";
}
},
methods:{
// 处理安全退出
logout(){
localStorage.removeItem("user");
location.href="/empAdmin/login.html";
},
addEmp(){
// console.log(this.emp);
// console.log(this.$refs.empPhoto.files[0]);
// 文件上传必须是post请求 entype必须为multipart/from-data

// 构造表单
var fromData = new FormData();
fromData.append("name",this.emp.name);
fromData.append("age",this.emp.age);
fromData.append("salary",this.emp.salary);
fromData.append("photo",this.$refs.empPhoto.files[0]);
axios({
method:"post",
url:"http://localhost:8080/empAdmin/emp/addEmp",
data:fromData,
headers:{'content-type':'multipart/form-data'}
}).then(res=>{
// console.log(res.data);
if(res.data.status){
if(window.confirm(res.data.msg+"点击跳转员工列表页面")){
location.href="/empAdmin/emplist.html";
}else{
location.reload();
}
}else{
alert(res.data.msg);
}
})
}
}
})
</script>

修改显示用户头像的标签路径

1
2
3
<td>
<img :src="'/empAdmin/photos/'+emp.photoPath" style="height: 60px;">
</td>

3、删除员工

dao层

1
2
3
4
5
// 删除员工
void delete(String id);

// 根据id查询员工
Emp queryEmpById(String id);
1
2
3
4
5
6
7
8
<delete id="delete" parameterType="String" >
delete from t_emp where id = #{id}
</delete>

<select id="queryEmpById" resultType="Emp">
select id,name,photo_path,salary,age from t_emp
where id = #{id}
</select>

service层

1
2
3
4
5
// 删除员工
void delete(String id);

// 根据id查询员工
Emp queryEmpById(String id);
1
2
3
4
5
6
7
8
9
@Override
public void delete(String id) {
empDao.delete(id);
}

@Override
public Emp queryEmpById(String id) {
return empDao.queryEmpById(id);
}

controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@GetMapping("delete")
public Map<String,Object> delete(String id){
HashMap<String, Object> map = new HashMap<>();
try {
// 删除员工头像
Emp emp = empService.queryEmpById(id);
File file = new File(emp.getPhotoPath());
if(file.exists()){
file.delete();
}
empService.delete(id);
map.put("status",true);
map.put("msg","删除成功");
} catch (Exception e) {
e.printStackTrace();
map.put("status",false);
map.put("msg","删除失败");
}
return map;
}

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 删除员工
deleteEmp(id) {
if (window.confirm("确认删除吗?")) {
axios.get("http://localhost:8080/empAdmin/emp/delete?id=" + id)
.then(res => {
if (res.data.status) {
alert(res.data.msg);
location.reload();
} else {
alert(res.data.msg);
}
})
}
}

参数传递:

1
<a href="javascript:;" @click="deleteEmp(emp.id)">删除</a>

4、修改员工

dao

1
2
// 修改员工
void update(Emp emp);
1
2
3
4
5
<update id="update" parameterType="Emp">
update t_emp set
name=#{name},photo_path=#{photoPath},salary=#{salary},age=#{age}
where id = #{id}
</update>

service

1
2
// 修改员工
void update(Emp emp);
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void update(Emp emp) {
if(StringUtils.isEmpty(emp.getName())){
throw new RuntimeException("请输入用户名!");
}
if(StringUtils.isEmpty(emp.getSalary())){
throw new RuntimeException("请输入薪资!");
}
if(StringUtils.isEmpty(emp.getAge())){
throw new RuntimeException("请输入年龄!");
}
empDao.update(emp);
}

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
@PostMapping("update")
public Map<String, Object> update(Emp emp, MultipartFile photo) {
HashMap<String, Object> map = new HashMap<>();
try {
// 头像保存
// 1、修改文件名
if (!ObjectUtils.isEmpty(photo)) {
String newFileName = UUID.randomUUID().toString() + "." +
FilenameUtils.getExtension(photo.getOriginalFilename());
// 2、图像上传
photo.transferTo(new File(realPath, newFileName));
// 3、设置头像访问地址
emp.setPhotoPath(newFileName);
}
empService.update(emp);
map.put("status", true);
map.put("msg", "修改成功!");
} catch (Exception e) {
e.printStackTrace();
map.put("status", false);
map.put("msg", "提示:"+e.getMessage());
}
return map;
}

前端

从员工列表中选中员工的id传递给修改页面:

1
<a href="javascript:;" @click="updateEmp(emp.id)">修改</a>
1
2
3
4
// 修改员工
updateEmp(id){
location.href="/empAdmin/updateEmp.html?id="+id;
}

在修改页面创建前(created()方法中)发起获取当前员工信息的请求

1
2
3
4
5
6
7
8
9
10
// 获取员工id
var start = location.href.lastIndexOf("=");
var id = location.href.substring(start+1);
// console.log(id);
// 查询当前员工显示在页面上
axios.get("http://localhost:8080/empAdmin/emp/getEmp?id="+id)
.then(res=>{
console.log(res.data)
this.emp = res.data;
})

使用v-model将返回的json数据绑定表单中的属性值

旧照片的显示和新照片上传

绑定更新事件

1
<input type="button" @click="updateEmp" class="button" value="更 新"/>
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
// 更新员工信息
updateEmp() {
var formData = new FormData();
formData.append("id",this.emp.id);
formData.append("name",this.emp.name);
formData.append("photoPath",this.emp.photoPath)
formData.append("photo",this.$refs.empPhoto.files[0]);
formData.append("salary",this.emp.salary);
formData.append("age",this.emp.age);
console.log(formData);
axios({
method: "post",
url:"http://localhost:8080/empAdmin/emp/update",
data: formData,
headers:{'content-type':'multipart/form-data'}
}).then(res=>{
if(res.data.status){
if(window.confirm(res.data.msg)){
location.href="/empAdmin/emplist.html";
}
}else{
alert(res.data.msg);
}
})
}

三、整合Redis缓存

1、导入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置Redis访问ip和端口

1
2
3
4
5
6
7
8
9
10
spring:
datasource:
username: root
password: 2824199842
url: jdbc:mysql://127.0.0.1:3306/emp?serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
redis:
host: 127.0.0.1
port: 6379

3、创建RedisCache类实现mybatis中的Cache接口,对操纵mybatis缓存

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.zyz.cache;

// 使用redis操纵mybatis缓存
public class RedisCache implements Cache {
// 对应mapper文件中的namespace
private String id;

public RedisCache(String id) {
this.id = id;
}

// 封装获取redisTemplate的方法
public RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate =(RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
return redisTemplate;
}

@Override
public String getId() {
System.out.println("当前缓存的id:"+id);
return this.id;
}

// 放入redis缓存
@Override
public void putObject(Object key, Object value) {
System.out.println("放入缓存的key-value"+key+"-"+value);
// 获取redisTemplate
getRedisTemplate().opsForHash().put(id,key.toString(),value);
}

// 从redis缓存中获取
@Override
public Object getObject(Object key) {
System.out.println("获取缓存的key-value"+key+"-"+getRedisTemplate().opsForHash().get(id,key.toString()));
return getRedisTemplate().opsForHash().get(id,key.toString());
}

// 删除指定缓存信息
@Override
public Object removeObject(Object o) {
return null;
}

// 清除缓存信息
@Override
public void clear() {
System.out.println("清除缓存");
getRedisTemplate().delete(id);
}

// 缓存大小
@Override
public int getSize() {
return getRedisTemplate().opsForValue().size(id).intValue();
}
}

4、创建工具类applicationContextUtils实现ApplicationContextAware获取工厂实例,并从中获取需要的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.zyz.utils;

@Component
public class ApplicationContextUtils implements ApplicationContextAware {

private static ApplicationContext applicationContext;
// 获取当前工厂
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

// 获取工厂中的实现类
public static Object getBean(String name){
return applicationContext.getBean(name);
}
}

存储在缓存中的实体类必须实现Serializable接口

5、在mapper文件中设置自定义的Redis缓存

1
2
<!-- 使用自定义的Redis缓存 -->
<cache type="com.zyz.cache.RedisCache"></cache>

主要界面

项目总结

做了什么?

  • 实现了管理员的注册和登录
  • 员工的增删改查功能

学到了什么?

  • 使用了Redis作为Mybatis的缓存,以前只是对Mybatis的缓存机制停留在了解阶段,这次使用上了之前学习的Redis数据库
  • 使用了vue对页面进行渲染,axios发起异步请求,熟悉了前后端分离系统的数据交互模式
  • 使用base64格式在浏览器端显示图片,需要在base64格式的图片前面加上data:image/png;base64,前缀
  • 学习到了使用ObjectUtils,StringUtils对一些属性进行非空判断
  • 使用 Exception+ResponseBody,讲异常返回给前端,给用户友好的提示
  • 使用localStorage保存用户登录信息
  • 在vue中构造表单,填充数据和文件,后端使用MultipartFile类接收前端的文件,并上传值到指定目录

遇到的问题,怎么解决的?

  • 上传用户照片的问题,在指定的上传目录发现了文件的存在,可是却在浏览器中显示不出来,通过查看数据库中员工表的信息,发现其他属性都存在,只有照片为空,通过在controller的添加方法打上断点,调试发现图片并没有保存到员工对象中,通过添加相应的set方法,最后解决了问题。
  • 在项目中我使用了date来显示当前的日期,而对前后端的交互中要频繁使用到data变量,在一次测试过程中就一不小心写错了,检查了很长时间才发现,真的差之一毫,谬之千里啊!

优化

添加日志输出功能

1
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);

在方法中打印日志:

1
LOGGER.info("用户登录,参数:{}", JSON.toJSON(user));

使用自定义注解并实现日志切面的方式打印日志:

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)// 运行时获取注解值
public @interface Log {

String value() default "";
}

LogAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
@Aspect
public class LogAspect {

private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);

@Pointcut("execution(public * com.zyz.controller..*.*(..))")
public void logPointcut() {
}

@Before("logPointcut()")
public void logAround(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Log log = method.getAnnotation(Log.class);
if(null != log){
LOGGER.info(log.value());
}
}
}

自定义缓存注解并实现缓存切面

支持单独设置缓存的过期时间
1、自定义RedisTemplate,指定序列化方式,防止出现乱码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory); // Json序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// String序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String序列化方式
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

2、自定义缓存注解

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)// 运行时获取注解值
public @interface Cache {
// key
String key();

// 有效时间 默认30分钟 单位 s
long expire() default 30*60 ;

}

3、实现缓存切面CacheAspect.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
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
@Aspect
@Component
public class CacheAspect {

@Autowired
private RedisTemplate redisTemplate;

@Pointcut(value = "@annotation(com.zyz.aop.cache.Cache)")
public void cachePointcut() {

}

@Around(value = "cachePointcut() && @annotation(cache)")
public Object cacheAround(ProceedingJoinPoint point, Cache cache) {
// 根据参数生成key
final String key = parseKey(cache.key());
Object value = null;
try {
// 从redis中获取缓存
value = redisTemplate.opsForValue().get(key);
} catch (Exception e) {
e.printStackTrace();
}
if (null != value) {
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Method method = methodSignature.getMethod();
return value;
}
try {
//等待返回结果
Object proceed = point.proceed();
if (proceed != null) {
try {
//设置缓存
redisTemplate.opsForValue().set(key, proceed, cache.expire(), TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}

/**
* redis的键值生成策略
*
* @param key key前缀
*/
private String parseKey(String key) {
// 使用uuid拼接保证key唯一
StringBuilder sb = new StringBuilder(key + ":"+ UuidUtils.getUuid());
return sb.toString();
}
}
1
2
3
4
5
6
public class UuidUtils {

public static synchronized String getUuid(){
return UUID.randomUUID().toString().replace("-","");
}
}

4、使用自定义注解:

1
2
@Cache(key="user:login",expire = 15*60)
public User login(User user) {}

生成的key-value: