办公自动化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工程设置
在webapp下新建一个测试页,添加配置运行
下面应用程序上下文该成/,找不到窗口拖大点!!!!!
到此工程结构配置完成。
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包
网页输入地址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底层设计
数据库设计 - 核心表
sys_role 角色表
数据
sys_node 功能菜单,节点表
数据
sys_role_node 角色和节点对照表
数据
数据库设计 - 完整表
adm_department 部门表
实际项目中字段很多,这里只创建最核心字段 ;adm开头行政管理模块;
数据
adm_employee 员工表
数据
sys_user 用户表
数据;m8总经理级别8,t开头是研发部用户,s开头是市场部,后面数字是序号级别,方便测试;
sys_role_user 用户和角色对照表
数据;role_id2对应的1,2,6是管理岗,其他是业务岗
基于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>
实现用户登录 - 注意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
创建一个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>
效果:实现了根据不同账号等级动态显示功能菜单,原理是传入账号在数据库中根据条件查询返回节点信息
技巧: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 {
}
}
请假流程数据库设计
请假流程
工作流程
设计约束
- 每一个请假对应一个审批流程
- 请假单创建后,按业务规则生成部门经理、总经理审批任务
- 审批任务的经办人只能审批自己辖区内的请假申请
- 所有审批任务"通过",代表请假已经批准
- 任意审批任务"驳回",其余审批任务取消,请假申请被驳回
- 请假流程中任意节点产生的操作都要生成对应的系统通知
设计表
adm_leave_form请假申请表
adm_process_flow处理流程表
sys_notice系统通知
实现Dao与数据交互
先创建3个数据表对应的实体类LeaveForm、ProcessFlow、Notice
然后创建Dao接口和对应的xml,并新建3个详细的测试用例类进行测试。
巴拉巴拉巴拉太多太多了但是逻辑比较简单不写了直接看代码吧
开发请假申请功能
LeaveFormService中根据员工等级进行判断处理
实现请假申请控制器
完整实现请假申请功能
使用SweetAlert2对话框设计页面