办公自动化OA系统

  • 办公自动化系统(Office Automation)是替代传统办公的解决方案
  • OA系统是利用软件技术构建的单位内部办公平台,用于辅助办公
  • 利用OA系统可将办公数据数字化,可极大提高办公流程执行效率

项目源码:https://gitee.com/pikachu2333/oa-system


文章目录

  • 办公自动化OA系统
  • 项目设计与需求分析
  • 办公自动化OA系统
  • 请假流程
  • 框架&组件
  • 开发阶段
  • 环境配置与准备
  • 创建Maven Web工程
  • Maven添加插件引用
  • 配置mybatis-config.xml
  • 开发MybatisUtils工具类
  • 测试
  • MyBatis整合Druid连接池
  • 整合Freemarker
  • 配置logback日志格式
  • 编码阶段
  • RBAC(Role-Based Access Control) - 基于角色的访问控制
  • 开发基于RBAC的权限控制模块
  • RBAC底层设计
  • 数据库设计 - 核心表
  • sys_role 角色表
  • sys_node 功能菜单,节点表
  • sys_role_node 角色和节点对照表
  • 数据库设计 - 完整表
  • adm_department 部门表
  • adm_employee 员工表
  • sys_user 用户表
  • sys_role_user 用户和角色对照表
  • 基于LayUI开发登录页
  • 实现用户登录 - 注意MVC架构模式的运用
  • 后台校验代码
  • 结合界面通过servlet完成校验任务
  • 分析后台首页布局与设计
  • 动态显示功能菜单
  • 技巧:XML配置下实现Mapper接口
  • 基于MD5算法对密码加密
  • MD5摘要算法
  • MD5特点
  • Apache Commons Codec
  • 敏感数据"加盐"混淆
  • 实现注销功能
  • 请假流程数据库设计
  • 请假流程
  • 工作流程
  • 设计约束
  • 设计表
  • 实现Dao与数据交互
  • 开发请假申请功能
  • 实现请假申请控制器
  • 完整实现请假申请功能
  • 请假审批功能


项目设计与需求分析

办公自动化OA系统

  • 办公自动化OA系统要求采用多用户B/S架构设计开发
  • HR为每一位员工分配系统账户,员工用此账户登录系统
  • 公司采用分级定岗,从1-8依次提升,不同岗位薪资水平不同
  • 6级(含)以下员工为业务岗,对应人员执行公司业务事宜
  • 7-8级为管理岗,其中7级为部门经理,8级为总经理
  • 业务岗与管理岗员工可用系统功能不同,要求运行灵活配置

请假流程

  • 公司所有员工都可以使用"请假申请"功能申请休假。
  • 所有员工都可以查看系统通知,申请请假的员工可以查看自己的请假审批进度,经理可以查看待审批的请假申请。
  • 员工请假时间少于72小时,部门经理审批后直接通过。
  • 员工请假时间大于72小时,部门经理审批后还需总经理进行审批。
  • 部门经理只允许批准本部门员工申请
  • 部门经理请假需直接由总经理审批。
  • 总经理提起请假申请,系统自动批准通过。

框架&组件

数据库:MySQL 8

ORM框架:Mybatis 3.5

数据库连接池:Alibaba Druid

J2EE web组件:Servlet 3.1

模板引擎:Freemarker 2.3

UI框架:LayUI 2.5

开发阶段

环境配置与准备

创建Maven Web工程

创建oa_system数据库

创建maven工程,jdk版本1.8

工程名oa-system,增加web工程设置

oa办公系统docker OA办公系统官方版_idea


oa办公系统docker OA办公系统官方版_sql_02

在webapp下新建一个测试页,添加配置运行

下面应用程序上下文该成/,找不到窗口拖大点!!!!!

oa办公系统docker OA办公系统官方版_sql_03

到此工程结构配置完成。

Maven添加插件引用

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>oa-system</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 配置阿里云仓库优先下载jar包 -->
    <repositories>
        <repository>
            <id>aliyun</id>
            <name>aliyun</name>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>

    <dependencies>
        <!-- mybatis 框架 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!-- MySQL 8 JDBC驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>
        <!-- 阿里巴巴Druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.14</version>
        </dependency>
        <!-- 单元测试组件 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <!-- 日志组件 -->
        <dependency>
            <groupId>org.jboss.pnc.build-agent</groupId>
            <artifactId>logback</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
</project>

配置mybatis-config.xml

src/main/resources/mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 官网复制DTD约束 -->
<!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">
            <!-- 事务管理器采用JDBC方式 -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 利用Mybatis自带的连接池管理连接 -->
            <dataSource type="POOLED">
                <!-- JDBC连接属性;mysql8之前去掉cj包 -->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/oa_system?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
</configuration>

开发MybatisUtils工具类

src/main/java/oa/utils/MybatisUtils.java

package oa.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.Reader;
import java.util.function.Function;

public class MybatisUtils {
    //static 保证全局唯一
    private static SqlSessionFactory sqlSessionFactory = null;
    //利用静态块在初始化类时实例化SqlSessionFactory
    static {
        try {
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException e) {
            //初始化错误时,通过抛出ExceptionInInitializerError通知调用者
            throw new ExceptionInInitializerError(e);
        }
    }

    //查询和修改方法封装了数据库的连接与关闭(回收),使用lambda表达式做参数,把要执行的sql语句部分代码放到调用时
    /**
     * 执行SELECT查询SQL;
     * @param func 要执行查询语句的代码块
     * @return 查询结构
     */
    public static Object executeQuery(Function<SqlSession, Object> func) {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            Object obj = func.apply(sqlSession);
            return obj;
        } finally {
            sqlSession.close();
        }
    }

    /**
     * 执行INDERT/UPDATE/DELETE写操作SQl
     * @param func 要执行写操作的代码块
     * @return 写操作后返回的结果
     */
    public static Object executeUpdate(Function<SqlSession, Object> func) {
        SqlSession sqlSession = sqlSessionFactory.openSession(false);   //参数false关闭自动提交事务
        try {
            Object obj = func.apply(sqlSession);
            sqlSession.commit();
            return obj;
        } catch (RuntimeException e) {
            sqlSession.rollback();
            throw e;
        } finally {
            sqlSession.close();
        }
    }
}
测试

resources/mappers/test.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 将之前的config DTD约束改为mapper DTD约束 -->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test">
    <select id="sample" resultType="String">
        select 'success'
        - 直接返回success字符串
    </select>
</mapper>

mybatis-config.xml 配置映射地址

<mappers>
        <mapper resource="mappers/test.xml"/>
    </mappers>

test/java/oa/utils/MybatisUtilsTestor.java

package oa.utils;

import org.junit.Test;

public class MybatisUtilsTestor {
    @Test
    public void testcase1() {
        String result = (String) MybatisUtils.executeQuery(sqlSession -> {
            String out = sqlSession.selectOne("test.sample");
            return out;
        });
        System.out.println(result);
    }
    
	//只有一条语句,简化写法
    @Test
    public void testcase2() {
        String result = (String) MybatisUtils.executeQuery(sqlSession -> sqlSession.selectOne("test.sample"));
        System.out.println(result);
    }
}

MyBatis整合Druid连接池

oa/datasource/DruidDataSourceFactory.java

package oa.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;

import javax.sql.DataSource;
import java.sql.SQLException;

public class DruidDataSourceFactory extends UnpooledDataSourceFactory {
    public DruidDataSourceFactory() {
        this.dataSource = new DruidDataSource();
    }

    @Override
    public DataSource getDataSource() {
        try {
            ((DruidDataSource)this.dataSource).init();  //初始化Druid数据源
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return this.dataSource;
    }
}

mybatis-config.xml 修改连接池配置

<dataSource type="oa.datasource.DruidDataSourceFactory">
                <!-- JDBC连接属性;mysql8之前去掉cj包 -->
                <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/oa_system?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
                <property name="initialSize" value="10"/>
                <property name="maxActive" value="20"/>
            </dataSource>

整合Freemarker

pom.xml 增加引用

<!-- Freemarker依赖 -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.29</version>
        </dependency>
        <!-- servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <!-- 依赖只参与编译测试,不进行发布 -->
            <scope>provided</scope>
        </dependency>

src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <servlet>
        <servlet-name>freemarker</servlet-name>
        <servlet-class>freemarker.ext.servlet.FreemarkerServlet</servlet-class>
        <init-param>
            <param-name>TemplatePath</param-name>
            <param-value>/WEB-INF/ftl</param-value>
        </init-param>
        <init-param>
            <!-- 设置读取ftl文件时采用的字符集 -->
            <param-name>default_encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>freemarker</servlet-name>
        <url-pattern>*.ftl</url-pattern>
    </servlet-mapping>
</web-app>

新建一个servlet测试:

src/main/java/oa/test/TestServlet.java

package oa.test;

import oa.utils.MybatisUtils;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;

@WebServlet(name = "TestServlet", urlPatterns = "/test")
public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String result = (String) MybatisUtils.executeQuery(sqlSession -> sqlSession.selectOne("test.sample"));
        request.setAttribute("result", result);
        request.getRequestDispatcher("test.ftl").forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }
}

记得添加发布后引用的这堆jar包

oa办公系统docker OA办公系统官方版_javaweb_04

网页输入地址http://localhost/test即可看到success,也就是刚刚写的案例sql代码返回的字符串,通过freemarker添加到网页中。

配置logback日志格式

resources/logback.xml 之前的项目已经写过,直接复制过来。

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!-- 指定在控制台中输出日志 -->
    <!-- name属性可以随意,如果要在控制台输出,一般称之为console -->
    <!-- class属性指定何处打印输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 编码节点 -->
        <encoder>
            <!--
                %d{HH:mm:ss.SSS}:输出时间格式,精确到毫秒
                [%thread]:当前操作的线程
                %-5level:以5个字符右对齐以及级别
                %logger{36}:具体哪个类的日志(只显示36个字符)
                %msg:日志信息
                %n:换行
                这些表达式在logback官网上都有详细说明
            -->
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <!--
        日志输出级别(优先级高到低):
        error: 错误 - 系统的故障日志
        warn: 警告 - 存在风险或使用不当的日志
        info: 一般性消息
        debug: 程序内部用于调试信息
        trace: 程序运行的跟踪信息
        下方root标签表示日志的最低输出级别为debug,即debug级别以下的信息不进行输出
    -->
    <root level="debug">
        <appender-ref ref="console"></appender-ref>
    </root>
</configuration>

编码阶段

RBAC(Role-Based Access Control) - 基于角色的访问控制

  • 基于角色权限控制(RBAC)是面向企业安全策略的访问控制方式
  • RBAC核心思想是将控制访问的资源与角色(Role)进行绑定
  • 系统的用户(User)与角色(Role)再进行绑定,用户便拥有对应权限

开发基于RBAC的权限控制模块

RBAC底层设计

oa办公系统docker OA办公系统官方版_javaweb_05

数据库设计 - 核心表
sys_role 角色表

oa办公系统docker OA办公系统官方版_xml_06

数据

oa办公系统docker OA办公系统官方版_oa办公系统docker_07

sys_node 功能菜单,节点表

oa办公系统docker OA办公系统官方版_sql_08

数据

oa办公系统docker OA办公系统官方版_idea_09

sys_role_node 角色和节点对照表

oa办公系统docker OA办公系统官方版_xml_10

数据

oa办公系统docker OA办公系统官方版_xml_11

数据库设计 - 完整表
adm_department 部门表

实际项目中字段很多,这里只创建最核心字段 ;adm开头行政管理模块;

oa办公系统docker OA办公系统官方版_xml_12

数据

oa办公系统docker OA办公系统官方版_oa办公系统docker_13

adm_employee 员工表

oa办公系统docker OA办公系统官方版_oa办公系统docker_14

数据

oa办公系统docker OA办公系统官方版_javaweb_15

sys_user 用户表

oa办公系统docker OA办公系统官方版_sql_16

数据;m8总经理级别8,t开头是研发部用户,s开头是市场部,后面数字是序号级别,方便测试;

oa办公系统docker OA办公系统官方版_oa办公系统docker_17

sys_role_user 用户和角色对照表

oa办公系统docker OA办公系统官方版_sql_18

数据;role_id2对应的1,2,6是管理岗,其他是业务岗

oa办公系统docker OA办公系统官方版_idea_19

基于LayUI开发登录页

LayUI前端框架:https://www.layui.com/

使用LayUI快速开发登录页面

引入下载的LayUI资源,放在main/webapp/resources目录

webapp下新建login.html登录页

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>办公自动化OA系统</title>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">
    <style>
        body{
          background-color: #f2f2f2;
        }
        .oa-container {
            /*background-color: white;*/
            position: absolute;
            width: 400px;
            height: 350px;
            top: 50%;
            left: 50%;
            padding: 20px;
            margin-left: -200px;
            margin-top: -175px;
        }
        #username,#password {
            text-align: center;
            font-size: 24px;
        }
    </style>
</head>
<body>
<div class="oa-container">
    <h1 style="text-align: center;margin-bottom: 20px;">办公自动化OA系统</h1>
    <form class="layui-form">
        <div class="layui-form-item">
            <input type="text" id="username" name="username" required lay-verify="required" placeholder="请输入账号" autocomplete="off" class="layui-input">
        </div>
        <div class="layui-form-item">
            <input type="password" id="password" name="password" required lay-verify="required" placeholder="请输入密码" autocomplete="off" class="layui-input">
        </div>
        <div class="layui-form-item">
            <!-- layui-btn-fluid 按钮与div同宽 -->
            <button class="layui-btn layui-btn-fluid" lay-submit lay-filter="login">登录</button>
        </div>
    </form>
</div>
</body>
</html>

oa办公系统docker OA办公系统官方版_idea_20

实现用户登录 - 注意MVC架构模式的运用

后台校验代码

创建数据库表对应的实体类

src/main/java/oa/entity/User.java

package oa.entity;

public class User {
    private Long userId;
    private String username;
    private String password;
    private Long employee;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Long getEmployee() {
        return employee;
    }

    public void setEmployee(Long employee) {
        this.employee = employee;
    }
}

mappers中定义新的sql语句

main/resources/mappers/user.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 将之前的config DTD约束改为mapper DTD约束 -->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="usermapper">
    <select id="selectByUsername" parameterType="String" resultType="oa.entity.User">
        select * from sys_user where username = #{value}
    </select>
</mapper>

mybatis-config.xml 添加user.xml引用

新建User数据库表操作类

src/main/java/oa/dao/UserDao.java

package oa.dao;

import oa.entity.User;
import oa.utils.MybatisUtils;

/**
 * 用户表Dao
 */
public class UserDao {
    /**
     * 按用户名名查询用户表
     * @param username 用户名
     * @return User对象包含对应的用户信息,null则代表对象不存在
     */
    public User selectByUsername(String username) {
        User user = (User)MybatisUtils.executeQuery(sqlSession -> sqlSession.selectOne("usermapper.selectByUsername", username));
        return user;
    }
}

新建UserService服务类

src/main/java/oa/service/UserService.java

package oa.service;

import oa.dao.UserDao;
import oa.entity.User;
import oa.service.exception.BussinessException;

public class UserService {
    private UserDao userDao = new UserDao();

    /**
     * 根据前台输入进行登录校验
     * @param username 前台输入用户名
     * @param password 前台输入密码
     * @return 校验通过后,包含对应用户数据的User实体类
     * @throws BussinessException L001-用户不存在,L002-密码错误
     */
    public User checkLogin(String username, String password) {
        User user = userDao.selectByUsername(username);
        if (user == null) {
            //用户不存在抛出自定义的业务逻辑异常
            throw new BussinessException("L001", "用户名不存在");
        }
        if (!password.equals(user.getPassword())) {
            throw new BussinessException("L002", "密码错误");
        }
        return user;
    }
}

这里抛出一个自定义的运行时异常类

service/exception/BussinessException.java

package oa.service.exception;

/**
 * 业务逻辑异常
 */
public class BussinessException extends RuntimeException{
    private String code;    //异常编码,异常的以为标识
    private String message; //异常的具体文本信息

    public BussinessException(String code, String message) {
        super(code + ":" + message);
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

新建一个测试用例类测试一下,在UserService类类名上按ctrl+shift+T生成测试用例类。

UserServiceTest.java

package oa.service;

import org.junit.Test;

import static org.junit.Assert.*;

public class UserServiceTest {
    private UserService userService = new UserService();

    @Test
    public void checkLogin1() {
        userService.checkLogin("uu", "23");
    }

    @Test
    public void checkLogin2() {
        userService.checkLogin("m8", "testf");
    }

    @Test
    public void checkLogin3() {
        userService.checkLogin("m8", "test");
    }
}
结合界面通过servlet完成校验任务

准备fastjson序列化组件

pom.xml

<!-- FastJSON JSON序列化组件 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>

创建控制服务类

java/oa/controller/LoginServlet.java

package oa.controller;

import com.alibaba.fastjson.JSON;
import oa.entity.User;
import oa.service.UserService;
import oa.service.exception.BussinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "LoginServlet", urlPatterns = "/check_login")
public class LoginServlet extends HttpServlet {
    //org.slf4j.Logger;日志输出器
    Logger logger = LoggerFactory.getLogger(LoginServlet.class);
    private UserService userService = new UserService();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        //接收用户输入
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        Map<String, Object> result = new HashMap<>();
        try {
            //调用业务逻辑
            User user = userService.checkLogin(username, password);
            result.put("code", "0");
            result.put("message", "success");
        } catch (BussinessException exception) {
            logger.error(exception.getMessage(), exception);
            result.put("code", exception.getCode());
            result.put("message", exception.getMessage());
        } catch (Exception exception) {
            logger.error(exception.getMessage(), exception);
            result.put("code", exception.getClass().getSimpleName());
            result.put("message", exception.getMessage());
        }
        //返回对应结果
        String json = JSON.toJSONString(result);
        response.getWriter().println(json);
    }
}

前端增加ajax请求跟后台交互;在最下面界面加载完后引入script

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>办公自动化OA系统</title>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">
    <style>
        body{
          background-color: #f2f2f2;
        }
        .oa-container {
            /*background-color: white;*/
            position: absolute;
            width: 400px;
            height: 350px;
            top: 50%;
            left: 50%;
            padding: 20px;
            margin-left: -200px;
            margin-top: -175px;
        }
        #username,#password {
            text-align: center;
            font-size: 24px;
        }
    </style>
</head>
<body>
<div class="oa-container">
    <h1 style="text-align: center;margin-bottom: 20px;">办公自动化OA系统</h1>
    <form class="layui-form">
        <div class="layui-form-item">
            <input type="text" id="username" name="username" required lay-verify="required" placeholder="请输入账号" autocomplete="off" class="layui-input">
        </div>
        <div class="layui-form-item">
            <input type="password" id="password" name="password" required lay-verify="required" placeholder="请输入密码" autocomplete="off" class="layui-input">
        </div>
        <div class="layui-form-item">
            <!-- layui-btn-fluid 按钮与div同宽 -->
            <button class="layui-btn layui-btn-fluid" lay-submit lay-filter="login">登录</button>
        </div>
    </form>
</div>
<script src="/resources/layui/layui.js"></script>
<script>
    layui.form.on("submit(login)", function (formdata) {//data表单包含了当前表单的的数据
        console.log(formdata);
        // 利用Ajax异步通信提交数据,LayUI内置了jQuery
        layui.$.ajax({
            url: "/check_login",
            data: formdata.field,
            type: "post",
            dataType: "json",
            success: function (json) {
                if (json.code == "0") {
                    layui.layer.msg("登录成功");
                } else {
                    layui.layer.msg(json.message);
                }
            }
        })
        return false;//submit提交实际返回true则表单提交,false则阻止本次提交,因为上面使用Ajax提交
    });
</script>
</body>
</html>

分析后台首页布局与设计

index页面设计…

动态显示功能菜单

分析:

1 - 如何获取用户编号为1的用户用有哪些功能?

用户sys_user,功能sys_node;通过两张关系表关联,sys_role_user通过用户找到对应的角色,通过用户拥有的角色在再sys_role_node找到对应的节点,在通过sys_role_node找到sys_node中的节点信息(三表关联)

select distinct n.*
from sys_role_user ru, sys_role_node rn, sys_node n
where ru.role_id = rn.role_id and user_id = 1 and rn.node_id = n.node_id
order by n.node_code

oa办公系统docker OA办公系统官方版_xml_21

创建一个node实体类

oa/entity/Node.java

package oa.entity;

public class Node  {
    private Long nodeId;
    private Integer nodeType;
    private String nodeName;
    private String url;
    private Integer nodeCode;
    private Long parentId;

    public Long getNodeId() {
        return nodeId;
    }

    public void setNodeId(Long nodeId) {
        this.nodeId = nodeId;
    }

    public Integer getNodeType() {
        return nodeType;
    }

    public void setNodeType(Integer nodeType) {
        this.nodeType = nodeType;
    }

    public String getNodeName() {
        return nodeName;
    }

    public void setNodeName(String nodeName) {
        this.nodeName = nodeName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Integer getNodeCode() {
        return nodeCode;
    }

    public void setNodeCode(Integer nodeCode) {
        this.nodeCode = nodeCode;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }
}

在项目mappers在新建xml查询文件,然后mybatis-config.xml添加引用

rbac.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!-- 将之前的config DTD约束改为mapper DTD约束 -->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="rbacmapper">
    <!-- 获取用户编号对应的功能列表 -->
    <select id="selectNodeByUserId" parameterType="Long" resultType="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 ru.user_id = #{value} and rn.node_id = n.node_id
    </select>
</mapper>

oa/dao/RbacDao.java 调用sql

package oa.dao;

import oa.entity.Node;
import oa.utils.MybatisUtils;

import java.util.List;

public class RbacDao {
    public List<Node> selectNodeByUserId(Long userId) {
        return (List<Node>) MybatisUtils.executeQuery(sqlSession -> sqlSession.selectList("rbacmapper.selectNodeByUserId", userId));
    }
}

打开UserService.java,因为刚才的Dao是selectNodeByUserId与用户是紧密相关的,所以调用放在这里。增加:

private RbacDao rbacDao = new RbacDao();    
	public List<Node> selectNodeByUserId(Long userId) {
        return rbacDao.selectNodeByUserId(userId);
    }

LoginServelt.java添加调整的地址,并将用户信息存入session等待跳转后页面index的使用

//调用业务逻辑
            User user = userService.checkLogin(username, password);
            HttpSession session = request.getSession();
            //向session存入登录用户信息,属性名 login_user
            session.setAttribute("login_user", user);
            result.put("code", "0");
            result.put("message", "success");
            result.put("redirect_url", "/index");

login.html登录成功增加跳转

window.location.href = json.redirect_url;

创建IndexServlet.java处理index请求

package oa.controller;

import oa.entity.Node;
import oa.entity.User;
import oa.service.UserService;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "IndexServlet", urlPatterns = "/index")
public class IndexServlet extends HttpServlet {
    private UserService userService = new UserService();
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("login_user");
        List<Node> nodeList = userService.selectNodeByUserId(user.getUserId());
        request.setAttribute("node_list", nodeList);
        request.getRequestDispatcher("/index.ftl").forward(request, response);

    }
}

WEB-INF/ftl/index.ftl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>慕课网办公OA系统</title>
    <link rel="stylesheet" href="/resources/layui/css/layui.css">
</head>

<body class="layui-layout-body">
<!-- Layui后台布局CSS -->
<div class="layui-layout layui-layout-admin">
    <!--头部导航栏-->
    <div class="layui-header">
        <!--系统标题-->
        <div class="layui-logo" style="font-size:18px">慕课网办公OA系统</div>
        <!--右侧当前用户信息-->
        <ul class="layui-nav layui-layout-right">
            <li class="layui-nav-item">
                <a href="javascript:void(0)">
                    <!--图标-->
                    <span class="layui-icon layui-icon-user" style="font-size: 20px">
                    </span>
                    <!--用户信息-->
<#--                    ${current_employee.name}[${current_department.departmentName}-${current_employee.title}]-->
                </a>
            </li>
            <!--注销按钮-->
            <li class="layui-nav-item"><a href="/logout">注销</a></li>
        </ul>
    </div>
    <!--左侧菜单栏-->
    <div class="layui-side layui-bg-black">
        <!--可滚动菜单-->
        <div class="layui-side-scroll">
            <!--可折叠导航栏-->
            <ul class="layui-nav layui-nav-tree">
                <#list node_list as node>
                    <#if node.nodeType == 1>
                        <!--父节点-->
                        <li class="layui-nav-item layui-nav-itemed">
                            <a href="javascript:void(0)">${node.nodeName}</a>
                            <dl class="layui-nav-child module" data-node-id="${node.nodeId}"></dl>
                        </li>
                    </#if>
                    <#if node.nodeType == 2>
                        <!--子节点-->
                        <dd class="function" data-parent-id="${node.parentId}">
                            <a href="javascript:void(0)" target="ifmMain">${node.nodeName}</a>
                        </dd>
                    </#if>
                </#list>

            </ul>
        </div>
    </div>
    <!--主体部分采用iframe嵌入其他页面-->
    <div class="layui-body" style="overflow-y: hidden">
        <iframe name="ifmMain" src="/forward/notice" style="border: 0px;width: 100%;height: 100%"></iframe>
    </div>
    <!--版权信息-->
    <div class="layui-footer">
        Copyright © imooc. All Rights Reserved.
    </div>
</div>
<!--LayUI JS文件-->
<script src="/resources/layui/layui.js"></script>
<script>
    //将所有功能根据parent_id移动到指定模块下
    layui.$(".function").each(function () {
        var func = layui.$(this);
        var parentId = func.data("parent-id");
        layui.$("dl[data-node-id=" + parentId + "]").append(func);
    });
    //刷新折叠菜单
    layui.element.render('nav');
</script>
</body>
</html>

效果:实现了根据不同账号等级动态显示功能菜单,原理是传入账号在数据库中根据条件查询返回节点信息

oa办公系统docker OA办公系统官方版_xml_22

技巧:XML配置下实现Mapper接口

动态显示右上角登录信息

获取员工

entity/Employee.java 创建员工实体类

package oa.entity;


public class Employee {
    private Long employeeId;
    private String name;
    private Long departmentId;
    private String title;
    private Integer level;

    public Long getEmployeeId() {
        return employeeId;
    }

    public void setEmployeeId(Long employeeId) {
        this.employeeId = employeeId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getDepartmentId() {
        return departmentId;
    }

    public void setDepartmentId(Long departmentId) {
        this.departmentId = departmentId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Integer getLevel() {
        return level;
    }

    public void setLevel(Integer level) {
        this.level = level;
    }
}

dao下创建EmployeeDao接口

EmployeeDao.java

package oa.dao;

import oa.entity.Employee;

public interface EmployeeDao {
    public Employee selectById(Long employee);
}

创建mappers/employee.xml并在mybatis-config.xml中注册

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace与包名一致-->
<mapper namespace="oa.dao.EmployeeDao1">
    <!--id与方法名一致;parameterType与方法参数类型对应;resultType与方法返回类型对应-->
    <select id="selectById" parameterType="Long" resultType="oa.entity.Employee">
        select * from adm_employee where employee_id = #{value }
    </select>
</mapper>

到此所有的底层工作准备完毕

创建service/EmployeeService.java employeeDao按照刚才书写的xml规则自动生成,通过getMapper获取接口对象

package oa.service;

import oa.dao.EmployeeDao;
import oa.entity.Employee;
import oa.utils.MybatisUtils;

public class EmployeeService {
    public Employee selectById(Long employeeId) {
        return (Employee) MybatisUtils.executeQuery(sqlSession -> {
            EmployeeDao employeeDao = sqlSession.getMapper(EmployeeDao.class);
            return employeeDao.selectById(employeeId);
        });
    }
}

获取部门

新建实体类entity/Department.java

package oa.entity;

public class Department {
    private Long departmentId;
    private String departmentName;

    public Long getDepartmentId() {
        return departmentId;
    }

    public void setDepartmentId(Long departmentId) {
        this.departmentId = departmentId;
    }

    public String getDepartmentName() {
        return departmentName;
    }

    public void setDepartmentName(String departmentName) {
        this.departmentName = departmentName;
    }
}

新建dao/DepartmentDao.java接口

package oa.dao;

import oa.entity.Department;

public interface DepartmentDao {
    public Department selectById(Long departmentId);
}

新建department.xml并注册

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace与包名一致-->
<mapper namespace="oa.dao.DepartmentDao">
    <!--id与方法名一致;parameterType与方法参数类型对应;resultType与方法返回类型对应-->
    <select id="selectById" parameterType="Long" resultType="oa.entity.Department">
        select * from adm_department where department_id = #{value }
    </select>
</mapper>

创建service/DepartmentService.java部门服务

package oa.service;

import oa.dao.DepartmentDao;
import oa.entity.Department;
import oa.utils.MybatisUtils;

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

打开IndexServlet在跳转页面之前获取当前登录对应的员工是哪个;获取部门名称

private UserService userService = new UserService();
    private EmployeeService employeeService = new EmployeeService();
    private DepartmentService departmentService = new DepartmentService();
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        //得到当前用户登录对象
        User user = (User) session.getAttribute("login_user");
        //获取当前登录员工对象
        Employee employee = employeeService.selectById(user.getEmployeeId());
        //获取员工对应部门
        Department department = departmentService.selectById(employee.getDepartmentId());
        //获取登录用户可用功能模块列表
        List<Node> nodeList = userService.selectNodeByUserId(user.getUserId());
        //放入请求属性
        request.setAttribute("node_list", nodeList);
        session.setAttribute("current_employee", employee);
        session.setAttribute("current_department", department);
        //请求派发至ftl进行展现
        request.getRequestDispatcher("/index.ftl").forward(request, response);
    }

index.ftl增加

<!--用户信息-->
                    ${current_employee.name}[${current_department.departmentName}-${current_employee.title}]

基于MD5算法对密码加密

MD5摘要算法
  • MD5信息摘要算法广泛使用的密码散列函数
  • MD5可以产生一个128位的散列值用于唯一标识源数据
  • 项目中通常使用MD5作为敏感数据的加密算法
MD5特点
  • 压缩性,MD5生成的摘要长度固定
  • 扛修改,源数据哪怕有一个字节变化,MD5也会有巨大差异
  • 不可逆,无法通过MD5反向推算源数据
Apache Commons Codec

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

  • Commons-Codec是Apache提供的编码/解码组件
  • 通过Commons-Codec可轻易生成源数据的MD5摘要
  • MD5摘要方法:String md5 = DigestUtils.md5Hex(源数据)

使用:

pom.xml maven增加依赖并在项目结构中添加

<!-- MD5摘要工具包 -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>

utils/MD5Utils.java 新建一个工具类

package oa.utils;


import org.apache.commons.codec.digest.DigestUtils;

public class MD5Utils {
    public static String md5Digest(String source) {
        return DigestUtils.md5Hex(source);
    }
}

ctrl+shift+T新建测试

@Test
    public void md5Digest() {
        System.out.println(MD5Utils.md5Digest("test"));
        System.out.println(MD5Utils.md5Digest("teat"));
        System.out.println(MD5Utils.md5Digest("test1"));
    }

可以得到MD5加密后的字符串。但是对于这些简短常用的字符串MD5的值是确定的容易被暴力破解,为了增强数据安全性,引入敏感数据加盐混淆。

敏感数据"加盐"混淆

MD5Utils.java

package oa.utils;


import org.apache.commons.codec.digest.DigestUtils;

public class MD5Utils {
    public static String md5Digest(String source) {
        return DigestUtils.md5Hex(source);
    }

    /**
     * 对原始字符串加盐后生成MD5摘要
     * @param source 原始字符串
     * @param salt 盐值
     * @return MD5摘要
     */
    public static String md5Digest(String source, Integer salt) {
        char[] ca = source.toCharArray();
        for(int i = 0; i < ca.length; i++) {
            ca[i] = (char) (ca[i] + salt);
        }
        String salted = new String(ca);
        return md5Digest(salted);
    }
}

将数据库中sys_user表新增salt盐值字段,将密码该为加盐后生成的加密值,每个用户盐值可以不一样;User.java实体类增加salt属性

UserService.java改为md5算法对密码进行判断

String md5 = MD5Utils.md5Digest(password, user.getSalt());
        if (!md5.equals(user.getPassword())) {
            throw new BussinessException("L002", "密码错误");
        }

实现注销功能

LogoutServlet.java 清除session信息并跳回登录页即可

package oa.controller;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;

@WebServlet(name = "LogoutServlet", urlPatterns = "/logout")
public class LogoutServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.getSession().invalidate();
        response.sendRedirect("/login.html");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }
}

请假流程数据库设计

请假流程

oa办公系统docker OA办公系统官方版_xml_23

工作流程

oa办公系统docker OA办公系统官方版_sql_24

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

adm_leave_form请假申请表

oa办公系统docker OA办公系统官方版_oa办公系统docker_25

adm_process_flow处理流程表

oa办公系统docker OA办公系统官方版_javaweb_26

sys_notice系统通知

oa办公系统docker OA办公系统官方版_javaweb_27

实现Dao与数据交互

先创建3个数据表对应的实体类LeaveForm、ProcessFlow、Notice

然后创建Dao接口和对应的xml,并新建3个详细的测试用例类进行测试。

巴拉巴拉巴拉太多太多了但是逻辑比较简单不写了直接看代码吧

开发请假申请功能

LeaveFormService中根据员工等级进行判断处理

实现请假申请控制器

完整实现请假申请功能

使用SweetAlert2对话框设计页面