需求说明

办公自动化OA系统

办公自动化系统(Office Automation)是代替传统办公的解决方案

OA系统是利用软件技术构建的单位内部办公平台,用于辅助办公

利用OA系统可将办公数据数字化,可极大提高办公流程执行效率

1672972750552

需求介绍

慕课办公OA系统要求采用B/S架构设计开发

HR为每一位员工分配系统账户,员工用此账户登陆系统

公司采用分级定岗,从1-8依次提升,不同岗位薪资水平不同

需求介绍-请假流程

6级(含)一下员工为业务岗,对应人员执行公司业务事宜

7-8级为管理岗,其中7级为部门经理,8级为总经理

业务岗与管理岗员工可用系统功能不同,要求允许灵活配置

公司所有员工都可以使用“请假申请”功能申请休假

请假时间少于72小时,部门经理审批后直接通过

请假时间大于72小时,部门经理审批后还需总经理进行审批

部门经理只允许批准本部门员工申请

部门经理请假需直接由总经理审批

总经理提起请假申请,系统自动批准通过

1672973243649

搭建基础架构

框架&组件

MySQL 8 Mybatis 3.5

Alibaba Druid Servlet 3.1

Vue 3.x Element Plus


1672973990261

1672974000531

1672974123811

1672975059489

1672975072812

1672975155662

1672975203944

1672975276420

1672975322830

MVC模式讲解

MVC架构模式

1672975399674

MVC架构模式优点

软件团队分工合作,成员各司其职

分层开发,显示与数据解耦,便于维护

组件可灵活替代,互不影响

基于MVC的软件分层设计

1672975814276

开发MyBatisUtils工具类

1672976869234

1672976403321

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 开启驼峰命名转换 form id -> formId -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/imooc_oa?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/test.xml"/>
</mappers>
</configuration>

1672991807274

1672993205104

实现MyBatisUtils工具类

1672991997650

1672992161403

1
2
3
4
5
6
7
8
9
<?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="test">
<select id="sample" resultType="String">
select 'success'
</select>
</mapper>

1672992236704

1672992423289

1672992824861

执行test

1672992852116

和test.xml文本中的一致

还可以写得更简洁

1672993037638

增删改类:

1672993572583


MyBatis整合Druid连接池

image-20230106234501718

image-20230106234607545

image-20230106234915833

image-20230106235254029

image-20230106235630023

这里为什么改成driverClassName?原因如下:

image-20230106235717811

运行试试:

image-20230106235937947

image-20230107000043910

登录与RBAC权限设计

RBAC权限底层设计

Role-Based Access Control

基于角色权限控制(RBAC)是面向企业安全策略的访问控制方式。

RBAC核心思想是将控制访问的资源与角色(Role)进行绑定。

系统的用户(User)与角色(Role)再进行绑定,用户便拥有对应权限。

RBAC底层设计

image-20230107000451478

RBAC数据表解析

image-20230107001902084

image-20230107001942612

image-20230107002222403
image-20230107002207006

image-20230107002635939

image-20230107002705051

image-20230107003049578

image-20230107003002244

初识ElementPlus

https://element-plus.gitee.io/zh-CN/

image-20230107003132838

1
2
3
4
5
6
7
8
<head>
<!-- 引入样式 -->
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" rel="external nofollow" target="_blank" />
<!-- 引入 Vue -->
<script src="//unpkg.com/vue@next" rel="external nofollow" ></script>
<!-- 引入组件库 -->
<script src="//unpkg.com/element-plus" rel="external nofollow" ></script>
</head>

实现登录功能

image-20230107004519283

image-20230107005212191

image-20230107005541108

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>办公OA系统</title>
<link rel="stylesheet" type="text/css" href="assets/element-plus/index.css"/>
<script src="assets/vue/vue.global.js"></script>
<script src="assets/element-plus/index.full.js"></script>
<script src="assets/axios/axios.js"></script>
<style>
.login-box{
border: 1px solid #dcdfe6;
width: 350px;
margin: 180px auto;
padding: 35px 35px 15px 35px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909300;
}
</style>
</head>
<body>
<div id="app">
<el-form ref="loginForm" label-width="80px" class="login-box">

</el-form>
</div>
<script>
const Main = {
data(){
return{

}
}
};
//初始化Vue,绑定Main中的数,利用ElementPlus对#app容器进行重新渲染
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app")
</script>
</body>
</html>

image-20230107010802371


绘制登录页面

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>办公OA系统</title>
<link rel="stylesheet" type="text/css" href="assets/element-plus/index.css"/>
<script src="assets/vue/vue.global.js"></script>
<script src="assets/element-plus/index.full.js"></script>
<script src="assets/axios/axios.js"></script>
<style>
.login-box {
border: 1px solid #dcdfe6;
width: 350px;
margin: 180px auto;
padding: 35px 35px 15px 35px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909399;
}
.login-title{
text-align: center;
margin: 0 auto 40px auto;
color: #303133;
}
</style>
</head>
<body>
<div id="app">
<el-form ref="loginForm" label-width="80px" :rules="rules" :model="form" class="login-box">
<h2 class="login-title">OA办公系统</h2>
<el-form-item label="账号" prop="username">
<el-input type="text" placeholder="请输入账号" v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" placeholder="请输入密码" v-model="form.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" v-on:click="onSubmit('loginForm')" style="width: 200px">登录</el-button>
</el-form-item>
</el-form>
</div>
<script>
const Main = {
data() {
return {
form: {
username: ''
, password: ''
}
, rules: {
username: [
{required: true, message: '账号不能为空', trigger: 'blur'}
],
password: [
{required: true, message: '密码不能为空', trigger: 'blur'}
]
}
}
}
, methods: {
onSubmit(formName) {
const form = this.$refs[formName];
form.validate((valid) => {
if (valid) {
console.info("表单校验成功,准备提交数据");
}
});
}
}
};
//初始化Vue,绑定Main中的数,利用ElementPlus对#app容器进行重新渲染
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app")
</script>
</body>
</html>

image-20230107112014643


实现用户登录Model层

image-20230107115151890

image-20230107115604473

image-20230107115656834

image-20230107115948282

1
2
3
4
5
6
7
8
9
<?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="usermapper">
<select id="selectByUsername" parameterType="String" resultType="org.example.oa.entity.User">
select * from sys_user where username = #{value}
</select>
</mapper>

image-20230107120034236

image-20230107120446307

1
2
3
4
5
6
public class UserMapper {
public User selectByUsername(String username){
User user = (User)MybatisUtils.executeQuery(sqlSession -> sqlSession.selectOne("usermapper.selectByUsername", username));
return user;
}
}

image-20230107134956456

1
2
3
4
5
public class LoginException extends RuntimeException{
public LoginException(String message){
super(message);
}
}

image-20230107135019589

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserService {
private UserMapper userMapper = new UserMapper();
/**
* 根据前台输入进行登录校验
* @param username 前台输入的用户名
* @param password 前台输入的密码
* @return 校验通过后,包含对应用户数据的User实体类
* @throws LoginException 用户登录异常
*/
public User checkLogin(String username, String password){
User user = userMapper.selectByUsername(username);
if (user == null){
throw new LoginException("用户名不存在");
}
if (!password.equals(user.getPassword())){
throw new LoginException("密码错误");
}
return user;
}
}

测试:

image-20230107135144668

image-20230107135217291

image-20230107135451852

image-20230107135515774

image-20230107135552520

image-20230107135558788

image-20230107135649417

image-20230107135701425

实现用户登录Controller层

image-20230107141459364

image-20230107141451762

image-20230107141804411

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
@WebServlet("/api/login")
public class LoginServlet extends HttpServlet {
private UserService userService = new UserService();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=utf-8");
//接收用户输入
String username = req.getParameter("username");
String password = req.getParameter("password");
//调用业务逻辑
Map result = new LinkedHashMap<>();
try {
User user = userService.checkLogin(username, password);
//处理结果编码,0表示处理成功,非0表示处理失败
result.put("code", "0");
result.put("message", "success");
} catch (Exception e) {
e.printStackTrace();
result.put("code", e.getClass().getSimpleName());
result.put("message", e.getMessage());
}
//返回JSON结果
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
String json = objectMapper.writeValueAsString(result);
resp.getWriter().println(json);
}
}

别忘记加入依赖:

image-20230107142112199

image-20230107142203873

image-20230107142235727

image-20230107142251774

image-20230107142714112

image-20230107142906802

image-20230107142952374

实现用户登录View层

回到login.html

image-20230107144003480

image-20230107144139187

image-20230107144554225

登录成功后跳转页面

image-20230107144637815

账号输错,会在上端报错

image-20230107144658113

密码输错,也会在上端报错

image-20230107144759407

封装ResponseUtils工具类

image-20230107153700755

image-20230107153835163

最后面加上

image-20230107154333521

对put()创建test对象

image-20230107154433904

1
2
3
4
5
6
7
8
9
public class ResponseUtilsTest {

@Test
public void put() {
ResponseUtils resp = new ResponseUtils("LoginException", "密码错误").put("class", "XXXClass").put("name", "mooc");
String json = resp.toJsonString();
System.out.println(json);
}
}

image-20230107212751464

还可以再简写为:

1
2
3
4
@Test
public void put2() {
System.out.println(new ResponseUtils("LoginException", "密码错误").put("class", "XXXClass").put("name", "mooc").toJsonString());
}

修改LoginServlet内容:

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
@WebServlet("/api/login")
public class LoginServlet extends HttpServlet {
private UserService userService = new UserService();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//接收用户输入
String username = request.getParameter("username");
String password = request.getParameter("password");
//调用业务逻辑
ResponseUtils resp = null;
try {
User user = userService.checkLogin(username, password);
//处理结果编码,0表示处理成功,非0表示处理失败
Map data = new LinkedHashMap();
data.put("user",user);
resp = new ResponseUtils().put("user",user);
} catch (Exception e) {
e.printStackTrace();
resp = new ResponseUtils(e.getClass().getSimpleName(), e.getMessage());
}
//返回JSON结果
response.getWriter().println(resp.toJsonString());
}
}

image-20230107220820601

image-20230107220836632

image-20230107220841758


封装Md5Utils加密工具类

MD5摘要算法

MD5信息摘要算法是广泛应用的密码散列函数

MD5可以产生出128位的散列值用于标识源数据

项目中通常使用MD5作为敏感数据的加密算法

http://md5.chahuo.com/

image-20230107223454882

Apache Commons Codec

Commons-Codec是Apache提供的编码/解码组件

通过Commons-Codec可轻易生成源数据的MD5摘要

MD5摘要方法: String md5 = DigestUtils.md5Hex(源数据)

https://commons.apache.org/proper/commons-codec/

在pom.xml中加入依赖

image-20230107224228479

别忘记把依赖加入lib

image-20230107224130319

image-20230107224357988

image-20230107224406801

生成测试用例类:

image-20230107224424376

image-20230107224527323

image-20230107224547691

但是这还不是很安全,如可以利用https://www.cmd5.com/

image-20230107224647906

所以这时候就要用到盐值(salt)

image-20230107225356146

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Md5Utils {
public static String md5Digest(String source){
return DigestUtils.md5Hex(source);
}

public static String md5Digest(String source, Integer salt){
char[] chars = source.toCharArray();
for (int i = 0; i < chars.length; i++){
chars[i] = (char) (chars[i] + salt);
}
String target = new String(chars);
System.out.println(target);
String md5 = DigestUtils.md5Hex(target);
return md5;
}
}

image-20230107225454681

image-20230107225645530

image-20230107225708715


完整实现登录功能

image-20230107225940491

这些密码就是进过加盐的。

修改UserServlet

image-20230107230759310

image-20230107230716258

测试看看:

image-20230107231141026

正确密码:

image-20230107231204446

image-20230107231229182

错误密码:

image-20230107231254008

image-20230107231303515

image-20230107231512073

image-20230107231530826

注意把返回数据设为null,防止被抓包。

image-20230107231759665

image-20230107231924127

这样password和salt就消失不见了。


实现后台首页

image-20230108000057920

image-20230108000227396

image-20230108000402849

image-20230108000957134

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>办公OA系统</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="assets/element-plus/index.css"/>
<!-- 引入组件库 -->
<script src="assets/vue/vue.global.js"></script>
<script src="assets/element-plus/index.full.js"></script>
<script src="assets/axios/axios.js"></script>
<style>
.el-header{
background-color: rgb(238, 241, 246);
color: #333;
line-height: 60px;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height:100%;border: 1px solid #eee">
<el-header>
1112321321
</el-header>
</el-container>
</div>
<script>
const Main = {
data(){
return{}
}
};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>

image-20230108001050489

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>办公OA系统</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="assets/element-plus/index.css"/>
<!-- 引入组件库 -->
<script src="assets/vue/vue.global.js"></script>
<script src="assets/element-plus/index.full.js"></script>
<script src="assets/axios/axios.js"></script>
<style>
.el-header{
background-color: rgb(238, 241, 246);
color: #333;
line-height: 60px;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height:100%;border: 1px solid #eee">
<el-header>
1112321321
</el-header>
<el-container>
<el-aside width="200px" style="max-height: 100%;background-color: rgb(238,241,246)" >
我是功能区
</el-aside>
<el-main>
我是页面显示区
</el-main>
</el-container>
</el-container>
</div>
<script>
const Main = {
data(){
return{}
}
};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>

image-20230108001412570

image-20230108001532647

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>办公OA系统</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="assets/element-plus/index.css"/>
<!-- 引入组件库 -->
<script src="assets/vue/vue.global.js"></script>
<script src="assets/element-plus/index.full.js"></script>
<script src="assets/axios/axios.js"></script>
<style>
.el-header{
background-color: rgb(238, 241, 246);
color: #333;
line-height: 60px;
}
html,body,#app,el-container{
padding: 0px;
margin: 0px;
height: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<div id="app">
<el-container style="height:100%;border: 1px solid #eee">
<el-header>
1112321321
</el-header>
<el-container>
<el-aside width="200px" style="max-height: 100%;background-color: rgb(238,241,246)" >
我是功能区
</el-aside>
<el-main>
我是页面显示区
</el-main>
</el-container>
</el-container>
</div>
<script>
const Main = {
data(){
return{}
}
};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>

image-20230108001628952

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<el-container style="height:100%;border: 1px solid #eee">
<el-header>
我是Header
</el-header>
<el-container>
<el-aside width="200px" style="max-height: 100%;background-color: rgb(238,241,246)" >
我是功能区
</el-aside>
<el-main>
<iframe id="main" name="main" src="http://www.baidu.com" style="width: 100%;height: 95%;border: 0px"></iframe>
</el-main>
</el-container>
</el-container>
</div>

image-20230108002225493

开发RBACModel层

查询员工可用功能。

image-20230108005412992

image-20230108005557708

image-20230108010152730

image-20230108010213204

image-20230108005727347

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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="rbacmapper">
<select id="selectNodeByUserId" parameterType="Long" resultType="org.example.oa.entity.Node">
select DISTINCT n.*
from sys_role_user ru, sys_role_node rn ,sys_node n
where
ru.role_id = rn.role_id and rn.node_id = n.node_id
and ru.user_id = #{value}
order by n.node_code
</select>
</mapper>

image-20230108010359350

image-20230108010716671

1
2
3
4
5
6
public class RbacMapper {
public List<Node> selectNodeByUserId(Long userId){
List list = (List) MybatisUtils.executeQuery(sqlSession -> sqlSession.selectList("rbacmapper.selectNodeByUserId", userId));
return list;
}
}

image-20230108010734006

1
2
3
4
5
6
public class RbacService {
private RbacMapper rbacMapper = new RbacMapper();
public List<Node> selectNodeByUserId(Long userId){
return rbacMapper.selectNodeByUserId(userId);
}
}

image-20230108010926819

1
2
3
4
5
6
7
8
9
10
public class RbacServiceTest {
private RbacService rbacService = new RbacService();
@Test
public void selectNodeByUserId() {
List<Node> nodes = rbacService.selectNodeByUserId(3l);
for (Node n:nodes){
System.out.println(n.getNodeName());
}
}
}

记得selectNodeByUserId(3l)里传入的是长整型,后面加l。

image-20230108011232844


开发RBACController层

image-20230108112659310

image-20230108113628566

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
@WebServlet("/api/user_info")
public class UserInfoServlet extends HttpServlet {
private RbacService rbacService = new RbacService();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String uid = request.getParameter("uid");
List<Node> nodes = rbacService.selectNodeByUserId(Long.parseLong(uid));
List<Map> treeList = new ArrayList<>();
Map module = null;
for (Node node : nodes){
if (node.getNodeType() == 1){
module = new LinkedHashMap();
module.put("node",node);
module.put("children", new ArrayList());
treeList.add(module);
} else if (node.getNodeType() == 2) {
List children = (List) module.get("children");
children.add(node);
}
}
String json = new ResponseUtils().put("nodeList", treeList).toJsonString();
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(json);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

image-20230108114050363

uid=1为管理岗

image-20230108114113141


实现RBACView层

先暂时注释掉跳转页面,方便调试:

image-20230108200036831

image-20230108200409037

image-20230108200432090

再次登录成功后:

image-20230108201052108

关闭浏览器后,这里的就被清除了。

本地存储(Local Storage)的是长期存储的

会话存储(Session Storage)的是短期存储的

来到index.html

image-20230108202027019

打开index.html控制台

image-20230108202108874

再次回到代码中:

image-20230108202545860

image-20230108202505901

push()将内容填充到了上面的nodeList:[]

导栏菜单:

image-20230108202815285

该源代码:

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
<template>
<el-row class="tac">
<el-col :span="12">
<h5 class="mb-2">Default colors</h5>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
>
<el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group title="Group One">
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>item four</template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>Navigator Two</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><document /></el-icon>
<span>Navigator Three</span>
</el-menu-item>
<el-menu-item index="4">
<el-icon><setting /></el-icon>
<span>Navigator Four</span>
</el-menu-item>
</el-menu>
</el-col>
<el-col :span="12">
<h5 class="mb-2">Custom colors</h5>
<el-menu
active-text-color="#ffd04b"
background-color="#545c64"
class="el-menu-vertical-demo"
default-active="2"
text-color="#fff"
@open="handleOpen"
@close="handleClose"
>
<el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group title="Group One">
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>item four</template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<span>Navigator Two</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><document /></el-icon>
<span>Navigator Three</span>
</el-menu-item>
<el-menu-item index="4">
<el-icon><setting /></el-icon>
<span>Navigator Four</span>
</el-menu-item>
</el-menu>
</el-col>
</el-row>
</template>

<script lang="ts" setup>
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from '@element-plus/icons-vue'
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath)
}
</script>

来到index.html中的功能区

image-20230108203445764

image-20230108204350201

image-20230108205107886

image-20230108205121163

换个低级用户试试:

image-20230108205212994

image-20230108205609484

image-20230108205634856

image-20230108205717549

image-20230108205723228


回顾Mapper接口开发过程

回到index.html头部

image-20230108212752105

image-20230108212813430

再往右上角显示登录人:

image-20230108213251237

image-20230108213302138

再往数据库中加载该sql,补充部门和员工信息。

image-20230108213452308

image-20230108213523259

image-20230108213555577

image-20230108213625901

image-20230108213635263

image-20230108214041561

image-20230108214400754

注意这里namespace用了接口名。

1
2
3
4
5
6
7
8
9
<?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="org.example.oa.mapper.EmployeeMapper">
<select id="selectById" parameterType="Long" resultType="org.example.oa.entity.Employee">
select * from adm_employee where employee_id = #{value}
</select>
</mapper>

image-20230108214542194

image-20230108214601687

别忘了注册:

image-20230108214719297

image-20230108214807580

再测试一下

image-20230108214839357

image-20230108215149929

image-20230108215238257


实现Header显示与注销功能

image-20230108215916365

1
2
3
4
5
6
7
8
9
public class EmployeeServlet {
public Employee selectById(Long employeeId){
Employee employee = (Employee) MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
return mapper.selectById(employeeId);
});
return employee;
}
}

来到之前的controller中:

image-20230108220004708

image-20230108220227341

image-20230108220437748

image-20230108220639945

接下来绑定页面

来到index.html

image-20230108221146853

来到上面进行双向绑定:

image-20230108221320738

image-20230108221410399

轮到注销了

image-20230108221838066

清空数据,回到登录页面:

image-20230108221633410


开发多级审批流程

请假流程

image-20230108225813883

设计约束

每一个单位对应一个审批流程

请假单创建后,按业务规则生成部门经理、总经理审批任务

审批任务的经办人只能审批自己辖区内的请假申请

所以审批任务”通过”,代表请假已经批准

任意审批任务”驳回”操作,其余审批任务取消,请假申请被驳回

请假流程中任意节点产生的操作都要生成对应的系统通知

请假流程数据库设计

设计表

image-20230108230827584

image-20230108230752617

image-20230109003935786

开发请假申请功能

image-20230109004008454

image-20230109004659634

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>请假申请单</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="/assets/element-plus/index.css">
<!-- 引入组件库 -->
<script src="/assets/vue/vue.global.js"></script>
<script src="/assets/element-plus/index.full.js"></script>
<script src="/assets/element-plus/locale/zh-cn.js"></script>
<script src="/assets/axios/axios.js"></script>

<style>
.el-form {
border: 1px solid #DCDFE6;
width: 600px;
margin: 180px auto;
padding: 35px 35px 15px 35px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
box-shadow: 0 0 25px #909399;
}
</style>

</head>
<body>
<div id="app">
<el-form ref="leaveForm" :model="form" :rules="rules" label-width="80px">
<el-descriptions title="请假申请单" :column="1" border>
<el-descriptions-item label="部门">研发部</el-descriptions-item>
<el-descriptions-item label="申请人">王美美[高级研发工程师]
</el-descriptions-item>
<el-descriptions-item label="请假类型">

<el-select v-model="form.formType" style="width: 100%">
<el-option label="事假" value="1"></el-option>
<el-option label="病假" value="2"></el-option>
<el-option label="工伤假" value="3"></el-option>
<el-option label="婚嫁" value="4"></el-option>
<el-option label="产假" value="5"></el-option>
<el-option label="丧假" value="6"></el-option>
</el-select>

</el-descriptions-item>
<el-descriptions-item label="请假时间">
<el-form-item prop="timeRange" label-width="0px">
<div class="block">
<el-date-picker
v-model="form.timeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期">
</el-date-picker>
</div>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="请假原因">
<el-form-item prop="reason" label-width="0px">
<el-input type="text" placeholder="请输入请假原因" v-model="form.reason"/>
</el-form-item>
</el-descriptions-item>

</el-descriptions>
<div style="text-align: center;padding-top: 30px">
<el-button type="primary" >立即申请</el-button>
</div>
</el-form>

</div>

<script>

var Main = {
data() {
return {
form: {
formType: "1",
timeRange: "",
startTime: "",
endTime: "",
reason: "",
eid: "",
},
// 表单验证,需要在 el-form-item 元素中增加 prop 属性
rules: {
timeRange: [
{required: true, message: '请选择请假时间', trigger: 'blur'}
],
reason: [
{required: true, message: '请填写请假原因', trigger: 'blur'}
]
}
}
}
};
ElementPlus.locale(ElementPlus.lang.zhCn);
const app = Vue.createApp(Main);
app.use(ElementPlus, ElementPlus.lang.zhCn);
app.mount("#app")
</script>
</body>
</html>

开发请假申请Mapper层

image-20230109010248978

image-20230109010300852

利用setter and getter和toString方法。

image-20230109010417558

image-20230109010427523

image-20230109011857830

image-20230109012336180

这里可以利用一条垃圾数据来生成insert语句方便写入。

image-20230109012500552

再进行修改,记得这里用不到form_id,将其删去

1
2
3
4
5
6
<mapper namespace="org.example.oa.mapper.LeaveFormMapper">
<insert id="insert" parameterType="org.example.oa.entity.LeaveForm">
INSERT INTO `imooc_oa`.`adm_leave_form` ( `employee_id`, `form_type`, `start_time`, `end_time`, `reason`, `create_time`, `state`)
VALUES ( #{employeeId}, #{formType}, #{startTime}, #{endTime}, #{reason}, #{createTime}, #{state});
</insert>
</mapper>

再完善一下,加入主键。

1
2
3
4
5
6
7
<mapper namespace="org.example.oa.mapper.LeaveFormMapper">
<insert id="insert" parameterType="org.example.oa.entity.LeaveForm"
useGeneratedKeys="true" keyProperty="formId" keyColumn="form_id">
INSERT INTO `imooc_oa`.`adm_leave_form` ( `employee_id`, `form_type`, `start_time`, `end_time`, `reason`, `create_time`, `state`)
VALUES ( #{employeeId}, #{formType}, #{startTime}, #{endTime}, #{reason}, #{createTime}, #{state});
</insert>
</mapper>

记得注册:

image-20230109013722018

测试一下

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 insert() {
MybatisUtils.executeUpdate(sqlSession -> {
LeaveFormMapper mapper = sqlSession.getMapper(LeaveFormMapper.class);
LeaveForm form = new LeaveForm();
form.setEmployeeId(4l); //员工编号
form.setFormType(1); //事假
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date startTime = null;//起始时间
Date endTime = null;//结束时间
try {
startTime = sdf.parse("2020-03-25 08:00:00");
endTime = sdf.parse("2020-04-01 18:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
form.setStartTime(startTime);
form.setEndTime(endTime);
form.setReason("回家探亲");//请假事由
form.setCreateTime(new Date());//创建时间
form.setState("processing");//当前状态
mapper.insert(form);
return null;
});
}

image-20230109105118531

image-20230109105427434

form_id它会自动加一。

image-20230109111302067

同之前一样,setter getter and toString

image-20230109111231127

image-20230109111240790

image-20230109111546560

insert语句还是从数据库中复制,并修改。

process_id和对应值要去掉。

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="org.example.oa.mapper.ProcessFormMapper">
<insert id="insert" parameterType="org.example.oa.entity.ProcessFlow"
useGeneratedKeys="true" keyProperty="processId" keyColumn="process_id">
INSERT INTO `imooc_oa`.`adm_process_flow` ( `form_id`, `operator_id`, `action`, `result`, `reason`, `create_time`, `audit_time`, `order_no`, `state`, `is_last`)
VALUES (#{formId}, #{operatorId}, #{action}, #{result}, #{reason}, #{createTime}, #{auditTime}, #{orderNo}, #{state}, #{isLast});
</insert>
</mapper>

记得注册:

image-20230109112119087

进行测试:

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

@Test
public void insert() {
MybatisUtils.executeUpdate(sqlSession -> {
ProcessFormMapper mapper = sqlSession.getMapper(ProcessFormMapper.class);
ProcessFlow processFlow = new ProcessFlow();
processFlow.setFormId(3l);
processFlow.setOperatorId(2l);
processFlow.setAction("audit");
processFlow.setResult("approved");
processFlow.setReason("同意");
processFlow.setCreateTime(new Date());
processFlow.setAuditTime(new Date());
processFlow.setOrderNo(1);
processFlow.setState("ready");
processFlow.setIsLast(1);
mapper.insert(processFlow);
return null;
});

image-20230109143039410

image-20230109143053755

再处理notice表

image-20230109143152428

这里不仅要get set toSting方法,还要含参无参构造方法

image-20230109143721743

这里含参的是这两个参数,再修改一下,删去Date参数。如下:

image-20230109143758417

image-20230109143923000

image-20230109144128386

image-20230109144621392

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="org.example.oa.mapper.NoticeMapper">
<insert id="insert" parameterType="org.example.oa.entity.Notice"
useGeneratedKeys="true" keyProperty="noticeId" keyColumn="notice_id">
INSERT INTO `sys_notice` (`receiver_id`, `content`, `create_time`)
VALUES (#{receiverId}, #{content}, #{createTime});
</insert>
</mapper>

注册:image-20230109144705174

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13


public class NoticeMapperTest {

@Test
public void insert() {
MybatisUtils.executeUpdate(sqlSession -> {
NoticeMapper mapper = sqlSession.getMapper(NoticeMapper.class);
mapper.insert(new Notice(2l, "测试消息"));
return null;
});
}
}

image-20230109145834143

image-20230109145920937


开发请假申请Service层

image-20230109155108838

image-20230109155115787

image-20230109160033584

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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="org.example.oa.mapper.EmployeeMapper">
<select id="selectById" parameterType="Long" resultType="org.example.oa.entity.Employee">
select * from adm_employee where employee_id = #{value}
</select>

<select id="selectByParams" parameterType="java.util.Map" resultType="org.example.oa.entity.Employee">
select * from adm_employee
where
1=1
<if test="level != null">
and level = #{level}
</if>
<if test="departmentId != null">
and department_id = #{department}
</if>
<if test="title != null">
and title = #{title}
</if>
</select>
</mapper>

都暂时先返回null,进行测试:

image-20230109162741685

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 EmployeeService {
public Employee selectById(Long employeeId){
Employee employee = (Employee) MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
return mapper.selectById(employeeId);
});
return employee;
}

public Employee selectLeader(Long employeeId){
MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = mapper.selectById(employeeId);
if (employee.getLevel() < 7){
//查询部门经理
}else if (employee.getLevel() == 7){
//查询总经理
}else if (employee.getLevel() == 8){
//返回自己
}
return null;
});
return null;
}
}

image-20230109163842091

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 LeaveFormService {
/**
* 创建请假单
* @param form 前端输入的请假单数据
* @return 持久化的请假单对象
*/
public LeaveForm createLeaveForm(LeaveForm form){
MybatisUtils.executeUpdate(sqlSession -> {
//1.持久化form表单数据,8级以下员工表单状态为processing,8级(总经理)状态为approved
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = employeeMapper.selectById(form.getEmployeeId());
if (employee.getLevel() == 8){
form.setState("approved");
}else {
form.setState("processing");
}
LeaveFormMapper leaveFormMapper = sqlSession.getMapper(LeaveFormMapper.class);
leaveFormMapper.insert(form);
//2.增加第一条流程数据,说明表单已提交,状态为complete
ProcessFlowMapper processFlowMapper = sqlSession.getMapper(ProcessFlowMapper.class);
ProcessFlow flow1 = new ProcessFlow();
flow1.setFormId(form.getFormId());
flow1.setOperatorId(employee.getEmployeeId());
flow1.setAction("apply");
flow1.setCreateTime(new Date());
flow1.setOrderNo(1);
flow1.setState("complete");
flow1.setIsLast(0);
processFlowMapper.insert(flow1);
//3.分情况创建其余流程数据
//3.1 7级以下员工,生成部门经理审批任务,请假时间大于等于72小时,还需生成总经理审批任务
//3.2 7级员工,仅生成总经理审批任务
//3.3 8级员工,生成总经理审批任务,系统自动通过
return null;
});
return null;
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void selectByParams1() {
Map params = new HashMap<>();
params.put("level", 7);
params.put("department", 2);
MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
List<Employee> employees = employeeMapper.selectByParams(params);
return employees;
});
}

@Test
public void selectByParams2() {
Map params = new HashMap<>();
params.put("level", 8);
MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
List<Employee> employees = employeeMapper.selectByParams(params);
return employees;
});
}

对于测试1:

image-20230109162603453

对于测试2:

image-20230109162643466

再回到EmployeeService补充完整

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
import org.example.oa.entity.Employee;
import org.example.oa.mapper.EmployeeMapper;
import org.example.oa.utils.MybatisUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class EmployeeService {
public Employee selectById(Long employeeId){
Employee employee = (Employee) MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
return mapper.selectById(employeeId);
});
return employee;
}

public Employee selectLeader(Long employeeId){
Employee l = (Employee)MybatisUtils.executeQuery(sqlSession -> {
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = mapper.selectById(employeeId);
Map params = new HashMap<>();
Employee leader = null;
if (employee.getLevel() < 7){
//查询部门经理
params.put("level", 7);
params.put("departmentId" ,employee.getDepartmentId());
List<Employee> employees = mapper.selectByParams(params);
leader = employees.get(0);
}else if (employee.getLevel() == 7){
//查询总经理
params.put("level", 8);
List<Employee> employees = mapper.selectByParams(params);
leader = employees.get(0);
}else if (employee.getLevel() == 8){
//返回自己
leader = employee;
}
return leader;
});
return l;
}
}

将LeaveFormService也补充完整

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
import org.example.oa.entity.Employee;
import org.example.oa.entity.LeaveForm;
import org.example.oa.entity.ProcessFlow;
import org.example.oa.mapper.EmployeeMapper;
import org.example.oa.mapper.LeaveFormMapper;
import org.example.oa.mapper.ProcessFlowMapper;
import org.example.oa.utils.MybatisUtils;

import java.util.Date;

public class LeaveFormService {
private EmployeeService employeeService = new EmployeeService();
/**
* 创建请假单
* @param form 前端输入的请假单数据
* @return 持久化的请假单对象
*/
public LeaveForm createLeaveForm(LeaveForm form){
LeaveForm f = (LeaveForm) MybatisUtils.executeUpdate(sqlSession -> {
//1.持久化form表单数据,8级以下员工表单状态为processing,8级(总经理)状态为approved
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = employeeMapper.selectById(form.getEmployeeId());
if (employee.getLevel() == 8){
form.setState("approved");
}else {
form.setState("processing");
}
LeaveFormMapper leaveFormMapper = sqlSession.getMapper(LeaveFormMapper.class);
leaveFormMapper.insert(form);
//2.增加第一条流程数据,说明表单已提交,状态为complete
ProcessFlowMapper processFlowMapper = sqlSession.getMapper(ProcessFlowMapper.class);
ProcessFlow flow1 = new ProcessFlow();
flow1.setFormId(form.getFormId());
flow1.setOperatorId(employee.getEmployeeId());
flow1.setAction("apply");
flow1.setCreateTime(new Date());
flow1.setOrderNo(1);
flow1.setState("complete");
flow1.setIsLast(0);
processFlowMapper.insert(flow1);
//3.分情况创建其余流程数据
//3.1 7级以下员工,生成部门经理审批任务,请假时间大于等于72小时,还需生成总经理审批任务
if (employee.getLevel() < 7){
Employee dmanager = employeeService.selectLeader(employee.getEmployeeId());
ProcessFlow flow2 = new ProcessFlow();
flow2.setFormId(form.getFormId());
flow2.setOperatorId(dmanager.getEmployeeId());
flow2.setAction("audit");
flow2.setCreateTime(new Date());
flow2.setOrderNo(2);
flow2.setState("process");
long diff = form.getEndTime().getTime() - form.getStartTime().getTime();
float hours = diff / (1000 * 60 * 60) * 1f;
if (hours >= 72) {
flow2.setIsLast(0);
processFlowMapper.insert(flow2);
Employee manager = employeeService.selectLeader(dmanager.getEmployeeId());
ProcessFlow flow3 = new ProcessFlow();
flow3.setFormId(form.getFormId());
flow3.setOperatorId(manager.getEmployeeId());
flow3.setAction("audit");
flow3.setCreateTime(new Date());
flow3.setState("ready");
flow3.setOrderNo(3);
flow3.setIsLast(1);
processFlowMapper.insert(flow3);
}else {
flow2.setIsLast(1);
processFlowMapper.insert(flow2);
}
} else if (employee.getLevel() == 7) {
//3.2 7级员工,仅生成总经理审批任务
Employee manager = employeeService.selectLeader(employee.getEmployeeId());
ProcessFlow flow2 = new ProcessFlow();
flow2.setFormId(form.getFormId());
flow2.setOperatorId(manager.getEmployeeId());
flow2.setAction("audit");
flow2.setCreateTime(new Date());
flow2.setState("process");
flow2.setOrderNo(2);
flow2.setIsLast(1);
processFlowMapper.insert(flow2);
} else if (employee.getLevel() == 8){
//3.3 8级员工,生成总经理审批任务,系统自动通过
ProcessFlow flow2 = new ProcessFlow();
flow2.setFormId(form.getFormId());
flow2.setOperatorId(employee.getEmployeeId());
flow2.setAction("audit");
flow2.setResult("approved");
flow2.setReason("自动通过");
flow2.setCreateTime(new Date());
flow2.setAuditTime(new Date());
flow2.setState("complete");
flow2.setOrderNo(2);
flow2.setIsLast(1);
processFlowMapper.insert(flow2);
}


return form;
});
return f;
}
}

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LeaveFormServiceTest {
LeaveFormService leaveFormService = new LeaveFormService();
@Test
public void createLeaveForm() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH");
LeaveForm form = new LeaveForm();
form.setEmployeeId(8l);
form.setStartTime(sdf.parse("2020032608"));
form.setEndTime(sdf.parse("2020040118"));
form.setFormType(1);
form.setReason("市场部员工请假单(72小时以上)");
form.setCreateTime(new Date());
LeaveForm savedForm = leaveFormService.createLeaveForm(form);
System.out.println(savedForm.getFormId());
}
}

输出了:10001

image-20230110003127374

image-20230110003310809

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void createLeaveForm2() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH");
LeaveForm form = new LeaveForm();
form.setEmployeeId(8l);
form.setStartTime(sdf.parse("2020032608"));
form.setEndTime(sdf.parse("2020032718"));
form.setFormType(1);
form.setReason("市场部员工请假单(72小时以内)");
form.setCreateTime(new Date());
LeaveForm savedForm = leaveFormService.createLeaveForm(form);
System.out.println(savedForm.getFormId());
}

image-20230110003538481

image-20230110003618810

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void createLeaveForm3() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH");
LeaveForm form = new LeaveForm();
form.setEmployeeId(2l);
form.setStartTime(sdf.parse("2020032608"));
form.setEndTime(sdf.parse("2020040118"));
form.setFormType(1);
form.setReason("研发部部门经理请假单");
form.setCreateTime(new Date());
LeaveForm savedForm = leaveFormService.createLeaveForm(form);
System.out.println(savedForm.getFormId());
}

image-20230110003937306

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void createLeaveForm4() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHH");
LeaveForm form = new LeaveForm();
form.setEmployeeId(1l);
form.setStartTime(sdf.parse("2020032608"));
form.setEndTime(sdf.parse("2020040118"));
form.setFormType(1);
form.setReason("总经理请假单");
form.setCreateTime(new Date());
LeaveForm savedForm = leaveFormService.createLeaveForm(form);
System.out.println(savedForm.getFormId());
}

image-20230110004240026


开发请假Controller层

image-20230110004444053

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
@WebServlet("/api/leave/*")
public class LeaveFormServlet extends HttpServlet {
private LeaveFormService leaveFormService = new LeaveFormService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//http://localhost/api/leave/create
String url = request.getRequestURI();
String methodName = url.substring(url.lastIndexOf("/") + 1);
if (methodName.equals("create")){
this.create(request, response);
} else if (methodName.equals("list")) {

} else if (methodName.equals("audit")) {

}
}
private void create(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String strEmployeeId = request.getParameter("eid");
String formType = request.getParameter("formType");
//从1970到现在的毫秒数
String startTime = request.getParameter("startTime");
String endTime = request.getParameter("endTime");
String reason = request.getParameter("reason");
LeaveForm form = new LeaveForm();
form.setEmployeeId(Long.parseLong(strEmployeeId));
form.setStartTime(new Date(Long.parseLong(startTime)));
form.setEndTime(new Date(Long.parseLong(endTime)));
form.setFormType(Integer.parseInt(formType));
form.setReason(reason);
form.setCreateTime(new Date());
ResponseUtils resp = null;
try {
leaveFormService.createLeaveForm(form);
resp = new ResponseUtils();
}catch (Exception e){
e.printStackTrace();
resp = new ResponseUtils(e.getClass().getSimpleName(), e.getMessage());
}
response.getWriter().println(resp.toJsonString());
}
}

开发请假申请View层

来到leave_form.html

中补充 employee:{}和mounted()

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
<script>

var Main = {
data() {
return {
employee:{},
department:{},
form: {
formType: "1",
timeRange: "",
startTime: "",
endTime: "",
reason: "",
eid: ""
},
// 表单验证,需要在 el-form-item 元素中增加 prop 属性
rules: {
timeRange: [
{required: true, message: '请选择请假时间', trigger: 'blur'}
],
reason: [
{required: true, message: '请填写请假原因', trigger: 'blur'}
]
}
}
}
,methods:{
changeTimeRange: function (){
console.log(this.form.timeRange);
}
}
,mounted(){
const objApp = this;
axios.get("/api/user_info?uid=" + sessionStorage.uid + "&eid=" + sessionStorage.eid)
.then(function(response){
console.info(response);
objApp.employee = response.data.data.employee;
objApp.department = response.data.data.department;
})
}
};
ElementPlus.locale(ElementPlus.lang.zhCn);
const app = Vue.createApp(Main);
app.use(ElementPlus, ElementPlus.lang.zhCn);
app.mount("#app");
</script>

image-20230110152730594

1
2
objApp.employee = response.data.data.employee;
objApp.department = response.data.data.department;

为传入数据,将姓名填充到申请单上。

增加部门类

image-20230110153647806

image-20230110153846637

增加部门接口

image-20230110153838001

image-20230110153858203

添加xml:

image-20230110154132194

image-20230110154148105

注册:

image-20230110154220244

添加业务

image-20230110154312458

1
2
3
4
5
6
7
8
9
10
11
12
package org.example.oa.service;

import org.example.oa.entity.Department;
import org.example.oa.mapper.DepartmentMapper;
import org.example.oa.utils.MybatisUtils;

public class DepartmentService {
public Department selectById(Long departmentId){
return (Department) MybatisUtils.executeQuery(sqlSession -> sqlSession.getMapper(DepartmentMapper.class).selectById(departmentId));
}
}

这样写语句虽然比较长,但比之前多行写的体验更好些。(开发小技巧)

最后在

image-20230110155039148

controller进行补充:

image-20230110155112081

image-20230110155209968

image-20230110162521084

开发请假审批功能

image-20230110145515401

在webapp中新建audit.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
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="/assets/element-plus/index.css">
<!-- 引入组件库 -->
<script src="/assets/vue/vue.global.js"></script>
<script src="/assets/element-plus/index.full.js"></script>
<script src="/assets/axios/axios.js"></script>
<style >

.info .el-col,.info .el-select ,.info .el-input{
padding-top: 5px;
padding-bottom: 5px;
}
</style>
</head>
<body>
<div id="app">
<h2>请假审批</h2>
<el-table
ref="singleTable"
:data="tableData"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%">
<el-table-column
type="index"
width="50">
</el-table-column>
<el-table-column
property="ctime"
label="申请时间"
width="180">
</el-table-column>
<el-table-column
property="ftype"
label="类型"
width="120">
</el-table-column>
<el-table-column
property="department_name"
label="部门"
width="120">
</el-table-column>
<el-table-column
property="name"
label="员工"
width="120">
</el-table-column>
<el-table-column
property="stime"
label="起始时间"
width="180">
</el-table-column>
<el-table-column
property="etime"
label="结束时间"
width="180">
</el-table-column>
<el-table-column
property="reason"
label="请假原因">
</el-table-column>
</el-table>

<el-dialog title="请假审批" v-model="dialogFormVisible" width="500px" center>
<el-descriptions :column="2" border>
<el-descriptions-item label="部门">{{currentRow.department_name}}</el-descriptions-item>
<el-descriptions-item label="姓名">{{currentRow.name}}</el-descriptions-item>
<el-descriptions-item label="起始时间" >{{currentRow.stime}}</el-descriptions-item>
<el-descriptions-item label="结束时间" >{{currentRow.etime}}</el-descriptions-item>
<el-descriptions-item label="请假原因" :span="2">
{{currentRow.reason}}
</el-descriptions-item>
</el-descriptions>


<div class="info" >
<el-form :model="form" ref="auditForm">
<el-select v-model="form.result" placeholder="是否同意" style="width: 100%">
<el-option label="同意" value="approved"></el-option>
<el-option label="驳回" value="refused"></el-option>
</el-select>
<el-input v-model="form.reason" placeholder="请输入审批意见" autocomplete="off"></el-input>
</el-form>
<span class="dialog-footer">
<el-button type="primary" v-on:click="onSubmit('auditForm')" style="width: 100%">确认提交</el-button>
</span>
</div>
</el-dialog>
</div>

<script>
function formatDate(time){
var newDate = new Date(time);
return newDate.getFullYear() + "-" +
(newDate.getMonth() + 1) + "-" + newDate.getDate()
+ " " + newDate.getHours() + "时";
}

var Main = {
data() {
return {
dialogFormVisible: false,
form: {
result:"approved",
reason:""
},
formLabelWidth: '120px',
tableData: [{
ctime:"2021-5-29 18时",
ftype:"事假",
stime:"2021-5-31 0时",
etime:"2021-6-3 0时",
department_name:"研发部",
name:"王美美",
reason:"测试数据"
}],
currentRow: null
}
}
,methods: {
handleCurrentChange(val) {
this.currentRow = val;
console.info(val);
this.dialogFormVisible = true;
}
}
};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app")
</script>

</body>
</html>

开发待审批表单Model层

image-20230110173203732

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="selectByParams" parameterType="java.util.Map" resultType="java.util.LinkedHashMap">
select f.*, e.name, d.*
from
adm_leave_form f,adm_process_flow pf, adm_employee e, adm_department d
where
f.form_id = pf.form_id
and f.employee_id = e.employee_id
and e.department_id = d.department_id
<if test="pf_state != null">
and pf.state = #{pf_state}
</if>
<if test="pf_operator_id != null">
and pf.operator_id = #{pf_operator_id}
</if>
</select>

image-20230110173240728

1
2
3
4
5
6
7
8
9
10
public interface LeaveFormMapper {
public void insert(LeaveForm form);
/*
public List<Map> selectByParams(Map params);
map.put("pf_operator_id", xxxx)
map.put("pf_state", xxxx)
*/
public List<Map> selectByParams(@Param("pf_state") String pfState
, @Param("pf_operator_id") Long pfOperatorId);
}

测试一下:

1
2
3
4
5
6
7
8
9
@Test
public void testSelectByParams(){
MybatisUtils.executeQuery(sqlSession -> {
LeaveFormMapper mapper = sqlSession.getMapper(LeaveFormMapper.class);
List<Map> list = mapper.selectByParams("process", 2l);
System.out.println(list);
return list;
});
}

image-20230110213101964

image-20230110213412316

1
2
3
4
5
6
7
public List<Map> getLeaveFormList(String pfState, Long operatorId){
return (List<Map>) MybatisUtils.executeQuery(sqlSession -> {
LeaveFormMapper mapper = sqlSession.getMapper(LeaveFormMapper.class);
List<Map> maps = mapper.selectByParams(pfState, operatorId);
return maps;
});
}

实现查询待审批请假单

image-20230110213550967

添加list方法

image-20230110221134189

1
2
3
4
5
6
7
8
9
10
11
12
private void list(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String employeeId = request.getParameter("eid");
ResponseUtils resp = null;
try{
List<Map> formList = leaveFormService.getLeaveFormList("process", Long.parseLong(employeeId));
resp = new ResponseUtils().put("list", formList);
}catch (Exception e){
e.printStackTrace();
resp = new ResponseUtils(e.getClass().getSimpleName(), e.getMessage());
}
response.getWriter().println(resp.toJsonString());
}

audit.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
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="/assets/element-plus/index.css">
<!-- 引入组件库 -->
<script src="/assets/vue/vue.global.js"></script>
<script src="/assets/element-plus/index.full.js"></script>
<script src="/assets/axios/axios.js"></script>
<style>

.info .el-col, .info .el-select, .info .el-input {
padding-top: 5px;
padding-bottom: 5px;
}
</style>
</head>
<body>
<div id="app">
<h2>请假审批</h2>
<el-table
ref="singleTable"
:data="tableData"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%">
<el-table-column
type="index"
width="50">
</el-table-column>
<el-table-column
property="ctime"
label="申请时间"
width="180">
</el-table-column>
<el-table-column
property="ftype"
label="类型"
width="120">
</el-table-column>
<el-table-column
property="department_name"
label="部门"
width="120">
</el-table-column>
<el-table-column
property="name"
label="员工"
width="120">
</el-table-column>
<el-table-column
property="stime"
label="起始时间"
width="180">
</el-table-column>
<el-table-column
property="etime"
label="结束时间"
width="180">
</el-table-column>
<el-table-column
property="reason"
label="请假原因">
</el-table-column>
</el-table>

<el-dialog title="请假审批" v-model="dialogFormVisible" width="500px" center>
<el-descriptions :column="2" border>
<el-descriptions-item label="部门">{{currentRow.department_name}}</el-descriptions-item>
<el-descriptions-item label="姓名">{{currentRow.name}}</el-descriptions-item>
<el-descriptions-item label="起始时间">{{currentRow.stime}}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{currentRow.etime}}</el-descriptions-item>
<el-descriptions-item label="请假原因" :span="2">
{{currentRow.reason}}
</el-descriptions-item>
</el-descriptions>


<div class="info">
<el-form :model="form" ref="auditForm">
<el-select v-model="form.result" placeholder="是否同意" style="width: 100%">
<el-option label="同意" value="approved"></el-option>
<el-option label="驳回" value="refused"></el-option>
</el-select>
<el-input v-model="form.reason" placeholder="请输入审批意见" autocomplete="off"></el-input>
</el-form>
<span class="dialog-footer">
<el-button type="primary" v-on:click="onSubmit('auditForm')" style="width: 100%">确认提交</el-button>
</span>
</div>
</el-dialog>
</div>

<script>
function formatDate(time) {
var newDate = new Date(time);
return newDate.getFullYear() + "-" +
(newDate.getMonth() + 1) + "-" + newDate.getDate()
+ " " + newDate.getHours() + "时";
}

var Main = {
data() {
return {
dialogFormVisible: false,
form: {
result: "approved",
reason: ""
},
formLabelWidth: '120px',
tableData: [{
ctime: "2021-5-29 18时",
ftype: "事假",
stime: "2021-5-31 0时",
etime: "2021-6-3 0时",
department_name: "研发部",
name: "王美美",
reason: "测试数据"
}],
currentRow: null
}
}
, methods: {
handleCurrentChange(val) {
this.currentRow = val;
console.info(val);
this.dialogFormVisible = true;
}
}
, mounted() {
const objApp = this;
const $message = this.$message;
axios.get("/api/leave/list?eid=" + sessionStorage.eid)
.then(function (response) {
const json = response.data;
if (json.code == '0') {
objApp.tableData.splice(0, objApp.tableData.length);
const formList = json.data.list;
formList.forEach(function (item) {
switch (item.form_type) {
case 1:
item.ftype = "事假";
break;
case 2:
item.ftype = "工伤假";
break;
case 3:
item.ftype = "事假";
break;
case 4:
item.ftype = "婚假";
break;
case 5:
item.ftype = "产假";
break;
case 6:
item.ftype = "丧假";
break;
}
objApp.tableData.push(item);
})
} else {
$message.error({message: json.message, offset: 100});
}

})
}


};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app")
</script>

</body>
</html>

开发请假审批Mapper层

image-20230110230955640

添加update语句

1
2
3
<update id="update" parameterType="org.example.oa.entity.LeaveForm">
UPDATE adm_leave_form SET employee_id = #{employeeId} , form_type = #{formType}, start_time = #{startTime}, end_time = #{endTime}, reason = #{reason}, state = #{state} ,create_time = #{createTime} WHERE form_id = #{formId}
</update>

image-20230110231042810

image-20230110231353025

image-20230110231417026

添加update语句

1
2
3
4
<update id="update" parameterType="org.example.oa.entity.ProcessFlow">
UPDATE adm_process_flow SET form_id = #{formId}, operator_id = #{operatorId}, action = #{action}, result = #{result}, reason = #{reason}, create_time = #{createTime}, audit_time = #{auditTime}, order_no = #{orderNo}, state = #{state}, is_last = #{isLast}
WHERE process_id = #{processId}
</update>

image-20230110231457277

image-20230110231447737

image-20230110231639542

再添加查询语句:

1
2
3
<select id="selectById" parameterType="Long" resultType="org.example.oa.entity.LeaveForm">
select * from adm_leave_form where form_id = #{value}
</select>

image-20230110231730202

image-20230110231808503

image-20230110231834100

添加查询语句:

1
2
3
<select id="selectByFormId" parameterType="Long" resultType="org.example.oa.entity.ProcessFlow">
select * from adm_process_flow where form_id = #{value} order by order_no
</select>

定义方法:

image-20230110232108530

开发请假审批Service层

image-20230110232303405

添加对应审核方法

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
package org.example.oa.service;

import org.example.oa.entity.Employee;
import org.example.oa.entity.LeaveForm;
import org.example.oa.entity.ProcessFlow;
import org.example.oa.mapper.EmployeeMapper;
import org.example.oa.mapper.LeaveFormMapper;
import org.example.oa.mapper.ProcessFlowMapper;
import org.example.oa.service.exception.LeaveFormException;
import org.example.oa.utils.MybatisUtils;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class LeaveFormService {
private EmployeeService employeeService = new EmployeeService();
/**
* 创建请假单
* @param form 前端输入的请假单数据
* @return 持久化的请假单对象
*/
public LeaveForm createLeaveForm(LeaveForm form){
LeaveForm f = (LeaveForm) MybatisUtils.executeUpdate(sqlSession -> {
//1.持久化form表单数据,8级以下员工表单状态为processing,8级(总经理)状态为approved
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = employeeMapper.selectById(form.getEmployeeId());
if (employee.getLevel() == 8){
form.setState("approved");
}else {
form.setState("processing");
}
LeaveFormMapper leaveFormMapper = sqlSession.getMapper(LeaveFormMapper.class);
leaveFormMapper.insert(form);
//2.增加第一条流程数据,说明表单已提交,状态为complete
ProcessFlowMapper processFlowMapper = sqlSession.getMapper(ProcessFlowMapper.class);
ProcessFlow flow1 = new ProcessFlow();
flow1.setFormId(form.getFormId());
flow1.setOperatorId(employee.getEmployeeId());
flow1.setAction("apply");
flow1.setCreateTime(new Date());
flow1.setOrderNo(1);
flow1.setState("complete");
flow1.setIsLast(0);
processFlowMapper.insert(flow1);
//3.分情况创建其余流程数据
//3.1 7级以下员工,生成部门经理审批任务,请假时间大于等于72小时,还需生成总经理审批任务
if (employee.getLevel() < 7){
Employee dmanager = employeeService.selectLeader(employee.getEmployeeId());
ProcessFlow flow2 = new ProcessFlow();
flow2.setFormId(form.getFormId());
flow2.setOperatorId(dmanager.getEmployeeId());
flow2.setAction("audit");
flow2.setCreateTime(new Date());
flow2.setOrderNo(2);
flow2.setState("process");
long diff = form.getEndTime().getTime() - form.getStartTime().getTime();
float hours = diff / (1000 * 60 * 60) * 1f;
if (hours >= 72) {
flow2.setIsLast(0);
processFlowMapper.insert(flow2);
Employee manager = employeeService.selectLeader(dmanager.getEmployeeId());
ProcessFlow flow3 = new ProcessFlow();
flow3.setFormId(form.getFormId());
flow3.setOperatorId(manager.getEmployeeId());
flow3.setAction("audit");
flow3.setCreateTime(new Date());
flow3.setState("ready");
flow3.setOrderNo(3);
flow3.setIsLast(1);
processFlowMapper.insert(flow3);
}else {
flow2.setIsLast(1);
processFlowMapper.insert(flow2);
}
} else if (employee.getLevel() == 7) {
//3.2 7级员工,仅生成总经理审批任务
Employee manager = employeeService.selectLeader(employee.getEmployeeId());
ProcessFlow flow2 = new ProcessFlow();
flow2.setFormId(form.getFormId());
flow2.setOperatorId(manager.getEmployeeId());
flow2.setAction("audit");
flow2.setCreateTime(new Date());
flow2.setState("process");
flow2.setOrderNo(2);
flow2.setIsLast(1);
processFlowMapper.insert(flow2);
} else if (employee.getLevel() == 8){
//3.3 8级员工,生成总经理审批任务,系统自动通过
ProcessFlow flow2 = new ProcessFlow();
flow2.setFormId(form.getFormId());
flow2.setOperatorId(employee.getEmployeeId());
flow2.setAction("audit");
flow2.setResult("approved");
flow2.setReason("自动通过");
flow2.setCreateTime(new Date());
flow2.setAuditTime(new Date());
flow2.setState("complete");
flow2.setOrderNo(2);
flow2.setIsLast(1);
processFlowMapper.insert(flow2);
}


return form;
});
return f;
}

public List<Map> getLeaveFormList(String pfState, Long operatorId){
return (List<Map>) MybatisUtils.executeQuery(sqlSession -> {
LeaveFormMapper mapper = sqlSession.getMapper(LeaveFormMapper.class);
List<Map> maps = mapper.selectByParams(pfState, operatorId);
return maps;
});
}

/**
* 审核请假单
* @param formId 表单编号
* @param operatorId 经办人(当前登录员工)
* @param result 审批结果
* @param reason 审批意见
*/
public void audit(Long formId, Long operatorId, String result, String reason){
MybatisUtils.executeUpdate(sqlSession -> {
ProcessFlowMapper processFlowMapper = sqlSession.getMapper(ProcessFlowMapper.class);
List<ProcessFlow> flowList = processFlowMapper.selectByFormId(formId);
if (flowList.size() == 0){
throw new LeaveFormException("无效的审批流程");
}
//获取当前任务ProcessFlow对象
List<ProcessFlow> processList = flowList.stream().filter(p->p.getOperatorId() == operatorId && p.getState().equals("process")).collect(Collectors.toList());
ProcessFlow process = null;
if (processList.size() == 0){
throw new LeaveFormException("未找到待处理任务节点");
}else {
process = processList.get(0);
process.setState("complete");
process.setResult(result);
process.setReason(reason);
process.setAuditTime(new Date());
processFlowMapper.update(process);
}

LeaveFormMapper leaveFormMapper = sqlSession.getMapper(LeaveFormMapper.class);
LeaveForm form = leaveFormMapper.selectById(formId);
//如果当前任务是最后一个节点,代表流程结束,更新请假单状态为对应的approved/refused
if (process.getIsLast() == 1){
form.setState(result); // approved / refused
leaveFormMapper.update(form);
}else {
//readyList包含所以后续任务节点
List<ProcessFlow> readyList = flowList.stream().filter(p -> p.getState().equals("ready")).collect(Collectors.toList());
//如果当前任务不是最后一个节点且审批通过,那下一个节点的状态从ready变为process
if (result.equals("approved")){
ProcessFlow readyProcess = readyList.get(0);
readyProcess.setState("process");
processFlowMapper.update(readyProcess);
} else if (result.equals("refused")) {
//如果当前任务不是最后一个节点且审批驳回,则后续所有任务状态变为cancel,请假单状态变为refused
for (ProcessFlow p:readyList){
p.setState("cancel");
processFlowMapper.update(p);
}
form.setState("refused");
leaveFormMapper.update(form);
}

}
return null;
});
}
}

处理异常方法:

image-20230111002323732

1
2
3
4
5
public class LeaveFormException extends RuntimeException{
public LeaveFormException(String message){
super(message);
}
}

实现请假审批功能

image-20230111003240603

image-20230111003336517

在最下面添加audit方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void audit(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String formId = request.getParameter("formId");
String result = request.getParameter("result");
String reason = request.getParameter("reason");
String eid = request.getParameter("eid");
ResponseUtils resp = null;
try {
leaveFormService.audit(Long.parseLong(formId),Long.parseLong(eid), result, reason);
resp = new ResponseUtils();
}catch (Exception e){
e.printStackTrace();
resp = new ResponseUtils((e.getClass().getSimpleName()),e.getMessage());
}
response.getWriter().println(resp.toJsonString());
}

image-20230111003735529

image-20230111003818394

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
,methods: {
handleCurrentChange(val) {
this.currentRow = val;
console.info(val);
this.dialogFormVisible = true;
}
,onSubmit(formName){
const objApp = this;
this.$refs[formName].validate(function(valid){
if(valid){
const params = new URLSearchParams();
params.append("formId", objApp.currentRow.form_id);
params.append("result", objApp.form.result);
params.append("reason", objApp.form.reason);
params.append("eid", sessionStorage.eid);
axios.post("/api/leave/audit" , params)
.then(function(response){
const json = response.data;
console.info(json);
if(json.code=="0"){
objApp.$alert("请假已审批完毕" , {
callback:function(){
window.location.href = "/notice.html";
}
})
}else{
objApp.$message.error({message:json.message,offset:100})
}
})
}
})
}
}

开发系统通知Service层

image-20230111103830863

image-20230111103856893image-20230111103910533

image-20230111104417323

image-20230111104405926

image-20230111104555528

1
String notice1 = String.format("您的请假申请[%s-%s]已提交,请等待上级审批", sdf.format(form.getStartTime()), sdf.format(form.getEndTime()));

image-20230111104700779

image-20230111105045078

1
2
3
4
String notice1 = String.format("您的请假申请[%s-%s]已提交,请等待上级审批", sdf.format(form.getStartTime()), sdf.format(form.getEndTime()));
noticeMapper.insert(new Notice(employee.getEmployeeId(), notice1));
String notice2 = String.format("%s-%s提起申请[%s-%s],请尽快审批", employee.getTitle(), employee.getName(), sdf.format(form.getStartTime()), sdf.format(form.getEndTime()));
noticeMapper.insert(new Notice(dmanager.getEmployeeId(),notice2));

对于部门经理的也差不多

image-20230111105249555

而总经理是自动通过

image-20230111105634592

来到下面audit方法

image-20230111110241203

image-20230111110609095

image-20230111110833087

image-20230111111031956

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if(result.equals("approved")){
ProcessFlow readyProcess = readyList.get(0);
readyProcess.setState("process");
processFlowMapper.update(readyProcess);
//消息1: 通知表单提交人,部门经理已经审批通过,交由上级继续审批
String notice1 = String.format("您的请假申请[%s-%s]%s%s已批准,审批意见:%s ,请继续等待上级审批" ,
sdf.format(form.getStartTime()) , sdf.format(form.getEndTime()),
operator.getTitle() , operator.getName(),reason);
noticeMapper.insert(new Notice(form.getEmployeeId(),notice1));

//消息2: 通知总经理有新的审批任务
String notice2 = String.format("%s-%s提起请假申请[%s-%s],请尽快审批" ,
employee.getTitle() , employee.getName() , sdf.format( form.getStartTime()) , sdf.format(form.getEndTime()));
noticeMapper.insert(new Notice(readyProcess.getOperatorId(),notice2));

//消息3: 通知部门经理(当前经办人),员工的申请单你已批准,交由上级继续审批
String notice3 = String.format("%s-%s提起请假申请[%s-%s]您已批准,审批意见:%s,申请转至上级领导继续审批" ,
employee.getTitle() , employee.getName() , sdf.format( form.getStartTime()) , sdf.format(form.getEndTime()), reason);
noticeMapper.insert(new Notice(operator.getEmployeeId(),notice3));

}

image-20230111111133209

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if (result.equals("refused")) {
//如果当前任务不是最后一个节点且审批驳回,则后续所有任务状态变为cancel,请假单状态变为refused
for (ProcessFlow p:readyList){
p.setState("cancel");
processFlowMapper.update(p);
}
form.setState("refused");
leaveFormMapper.update(form);
//消息1: 通知申请人表单已被驳回
String notice1 = String.format("您的请假申请[%s-%s]%s%s已驳回,审批意见:%s,审批流程已结束" ,
sdf.format(form.getStartTime()) , sdf.format(form.getEndTime()),
operator.getTitle() , operator.getName(),reason);
noticeMapper.insert(new Notice(form.getEmployeeId(),notice1));

//消息2: 通知经办人表单"您已驳回"
String notice2 = String.format("%s-%s提起请假申请[%s-%s]您已驳回,审批意见:%s,审批流程已结束" ,
employee.getTitle() , employee.getName() , sdf.format( form.getStartTime()) , sdf.format(form.getEndTime()), reason);
noticeMapper.insert(new Notice(operator.getEmployeeId(),notice2));
}

image-20230111111715732

image-20230111111722013

实现系统通知功能

image-20230111111850372

image-20230111112039701

image-20230111112101704

image-20230111112332313

image-20230111112337214

image-20230111112414452

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebServlet("/api/notice/list")
public class NoticeServlet extends HttpServlet {
private NoticeService noticeService = new NoticeService();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String employeeId = request.getParameter("eid");
ResponseUtils resp;
try {
List<Notice> noticeList = noticeService.getNoticeList(Long.parseLong(employeeId));
resp = new ResponseUtils().put("list", noticeList);
}catch (Exception e){
e.printStackTrace();
resp = new ResponseUtils(e.getClass().getSimpleName(),e.getMessage());
}
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(resp.toJsonString());

}
}

image-20230111113131872

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统通知</title>
<!-- 引入样式 -->
<link rel="stylesheet" type="text/css" href="/assets/element-plus/index.css">
<!-- 引入组件库 -->
<script src="/assets/vue/vue.global.js"></script>
<script src="/assets/element-plus/index.full.js"></script>
<script src="/assets/axios/axios.js"></script>
<script src="/assets/oa/security.js"></script>
</head>
<body>
<div id="app">
<h2>系统通知</h2>
<el-table
ref="singleTable"
:data="tableData"
highlight-current-row
style="width: 100%">
<el-table-column
property="index"
label="序号"
width="50">
</el-table-column>
<el-table-column
property="ctime"
label="通知时间"
width="180">
</el-table-column>

<el-table-column
property="content"
label="通知内容">
</el-table-column>
</el-table>

</div>

<script>

var Main = {
data() {
return {
tableData: []
}
}
,mounted() {
const objApp = this;
axios.get("/api/notice/list?eid=" + sessionStorage.eid)
.then(function (response) {
objApp.tableData.splice(0, objApp.tableData.length);
response.data.data.list.forEach(function (item,index) {
var date = new Date(item.createTime);
item.ctime = date.getFullYear() + "-" +
(date.getMonth() + 1) + "-" + date.getDate()
+ " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();
item.index = index + 1;
objApp.tableData.push(item);
});
})
.catch(function (error) {
console.log(error);
});
}
};
const app = Vue.createApp(Main);
app.use(ElementPlus);
app.mount("#app")
</script>

</body>
</html>

image-20230111113522419

image-20230111113530597

实现系统登录拦截功能

没有登录也能直接访问到后台

image-20230111205019303

image-20230111205230955

1
2
3
if(sessionStorage.eid == null || sessionStorage.uid == null){
window.location.href = "/login.html";
}

image-20230111205407621

image-20230111205453840

都引入。

对于/api/

image-20230111205640793

可基于令牌机制对接口进行授权,涉及架构,以后再涉猎。

项目面试题解析

请说明你的项目及使用技术

image-20230111210026682

1.严格和明确的需求说明书;

2.开发前,明确,技术栈。

image-20230111210032864

3.准备一个数据库,逻辑空间。

4.创建一个maven工程(管理本项目的各种依赖);设置为web工程;在IDEA中设置Tomcat

5.引入Mybatis:在maven的pom.xml中,引入mybatis依赖(相当于一个第三方jar包);创建mybatis-config.xml文件,配置Mybatis;创建MyBatisUtils工具类,解析和应用mybatis-config.xml文件;(MyBatisUtils工具类中的executeQuery()方法和executeUpdate()方法,使用到了Lambda表达式;因为,executeQuery()方法和executeUpdate()方法只是定义了一个范本,具体的实现需要在调用这些方法的时候再编写,所以这儿使用了Lambda表达式)

6.Mybatis整合Druid连接池:通过maven,在pom.xml中引入Druid依赖;创建Druid和Mybatis兼容所需的数据源工厂类:DruidDataSourceFac

7.引入FreeMarker:因为,本项目前端的模板引擎我们使用的是FreeMarker(没有使用JSP);引入FreeMarker的依赖;在web.xml中配置FreemarkerServlet(要想使freemarker在web项目中起作用,需要对freemarker.jar中的FreemarkerServlet的这个Servlet类进行配置啦);

8.引入Servlet:在pom.xml中引入Servlet依赖,并且该依赖的设置为provided(为web容器(比如Tomcat)本身就是一个Servlet容器,其自带了Servlet-apr.jar。所以,我们这儿引入的Servlet依赖只需要在程序编译的时候用到,并不需要将这个Servlet依赖发布到最终的运行环境中,因为Tomcat已经自带了。) ;

9.需要设置IDEA发布中包括maven的jar包,因为工程中引入的jar包大部分也是需要被包含在工程发布中的;(然后,以后项目中每引入一个依赖,并且该jar需要随工程发布的时候,都需要设置一下)

10.在pom.xml中引入logback日志依赖,并创建logback.xml设置一下;

你的项目有哪些亮点

  • 项目基于MVC模式进行开发,通过MVC的设计使得各层之间有效解藕。
  • 使用了Vue3、Element Plus前端技术。
  • 本项目主要是请假流程的模块,针对于请假流程,设计了process flow任务流进行处理,这是底层设计的亮点

请说明你对MVC的理解

image-20230111210303623

image-20230111210316138

每一层各司其职,从后往前依次推进(不允许出现夸层调用):

  • 最底层是数据持久层,使用MyBatis的mapper完成数据库的增删改查。
  • 业务逻辑层是负责处理响应业务逻辑的地方,并且是完成业务的主体方法。
  • 控制层:接收前端传来的数据、根据前端传来的数据调用相应的业务逻辑、最后将业务逻辑返回的数据进行包装,返回给前端。
  • 视图层指责为:向Servlet提交数据、从Servlet接收相应数据显示出来,前端进行渲染。

请你说明RBAC的实现原理

image-20230111210621330

RBAC的核心是角色,角色是进行数据绑定核心。

请阐述项目的工作流程设计

请假流程

  • 部门经理只允许批准本部门员工申请

  • 部门经理请假需直接由总经理审批

  • 总经理提起请假申请,系统自动批准通过

  • img

    设计约束

    • 每个请假单位对应一个审批流程
    • 请假单创建后,按业务规则生成部门经理、总经理审批任务
    • 审批任务的经办人只能审批自己辖区内的请假申请
    • 所有审批任务“通过”,代表请假已经批准
    • 任意审批任务“驳回”操作,其余审批任务取消,请假申请被驳回
    • 请假流程中任意节点产生的操作都要生成对应的系统通知

项目如何对敏感数据进行加密的

image-20230111211138243

image-20230111211150081