企业级saas模式hrm系统

目录

项目亮点

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

1 初识SaaS

1.1 云服务的三种模式

1.1.1 IaaS(Infrastructure as a Service)即(基础设施即服务)

1.1.2 PaaS(Platform-as-a-Service)即(平台即服务)

1.1.3 SaaS(Software-as-a-Service)(软件即服务)

4 工程搭建

4.1 前置知识点的说明

4.2 开发环境要求

4.2.1 lombok 插件

4.3 构建父工程

4.4 构建公共子模块

4.4.1 构建公共子模块ihrm-common

4.4.2 创建返回结果实体类

4.4.3 返回码定义类 (是枚举类型)

4.4.4 分布式ID生成器

 4.5 搭建公共的实体类模块

5 企业微服务-企业CRUD

5.1 模块搭建

5.2 企业管理-CRUD

5.2.1 表结构分析

5.2.2 完成企业增删改查操作

5.3公共异常处理

5.4跨域处理

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

知识点:

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

1.1 多租户是什么

1.2 需求分析

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

1.3.1 独立数据库

1.3.2 共享数据库、独立 Schema

​编辑

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

1.4 SAAS-HRM数据库设计

2 数据库设计与建模

2.1 数据库设计的三范式

2.2 数据库建模

2.2.1 建模工具

2.2.2 使用pd建模

第四章 权限管理与jwt鉴权

4 常见的认证机制

4.1 HTTP Basic Auth

4.2 Cookie Auth

4.3 OAuth

4.4 Token Auth

5 HRM中的TOKEN签发与验证

5.1 什么是JWT

5.2 JJWT的快速入门

5.2.2 token的解析

5.3 JWT工具类

5.4 登录成功签发token

5.5 获取用户信息鉴权

3 前端框架

3.2 启动与安装

3.3 工程结构

3.4 执行流程分析

3.4.1 路由和菜单

3.4.2 前端数据交互

4 企业管理

4.1 需求分析

4.2 搭建环境

4.2.1 新增模块

4.2.2 构造模拟数据

4.2.3 注册模块

4.2.4 配置路由菜单

4.2.5 编写业务页面

4.3 企业操作

4.3.1 创建api

4.3.2 企业列表

4.3.3 企业详情

4.4 与后台对接测试

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

学习目标:

1 组织机构管理

1.1 需求分析

1.1.1 需求分析

1.2 微服务实现

1.2.1 抽取公共代码

1.2.2 实现基本CRUD操作

1.3 前端实现

1.3.1 创建模块

1.3.2 配置请求API

1.3.3 构造列表

1.3.4 组织机构的增删改查

2 RBAC模型

2.1 什么是RBAC

2.2 基于RBAC的设计思路

2.3 表结构分析

SaaS-用户管理

3 SAAS-HRM中的权限设计

3.1 需求分析

3.1.1 SAAS平台的基本元素​编辑

3.2 权限设计

4 用户管理

4.1 需求分析

4.2 配置系统微服务

4.3 后端用户基本操作

4.4 前端用户基本操作

4.4.2 配置接口请求路径

4.4.1 导入员工模块

4.4.2 用户列表展示

4.4.4 用户详情

4.4.3 用户的新增

第4章 权限管理与jwt鉴权

1权限管理

1.1需求分析

1.2后端实现

2、分配角色

3、分配权限

4、常见的认证机制

<1>、HTTP Basic Auth

<2>、Cookie Auth

<3>、OAuth

<4>、Token Auth

5HRM中的TOKEN签发与验证

5.1什么是JWT

5.2JJWT的快速入门

5.2.1token的创建

5.2.2token的解析

5.2.3自定义claims

5.3JWT工具类

5.4登录成功签发token

5.5获取用户信息鉴权

 第五章 权限管理与shiro入门

1、前端权限控制

<1>、需求分析

(1)、需求说明

(2)、实现方案

<2>、服务端实现

<3>、前端实现

(1)、路由钩子函数

(2)、配置菜单权限

(3)、配置验证权限的方法

(4)、修改登录和获取信息的请求接口

1.4 权限测试

<4>、权限测试

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

<1>、什么是服务中的状态

<2>、无状态服务

<3>、有状态服务

3、基于JWT的API鉴权

3.1、基于拦截器的token与鉴权

<1>Spring中的拦截器

<2>、签发用户API权限

<3>、拦截器中鉴权

4、Shiro安全框架

<1>、Shiro概述

(1)、什么是Shiro

 (2)、与Spring Security的对比

(3)、Shiro的功能模块

<2>、Shiro的内部结构

<3>、应用程序使用Shiro

<4>、Shiro入门

(1)、搭建基于ini的运行环

(2)、用户认证

(3)、用户授权

(4)、自定义Realm

(5)、认证与授权的执行流程分析

[1]、认证流程

[2]、授权流程

​编辑

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

1Shiro在SpringBoot工程的应用

1.1案例说明

1.2整合Shiro

1.2.1spring和shiro的整合依赖

1.2.2修改登录方法

1.2.3自定义realm

1.3Shiro的配置

1.5授权

1.5.2基于注解的授权

2Shiro中的会话管理

2.1什么是shiro的会话管理

2.2应用场景分析

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

2.3.1 步骤分析

 2.3.2构建环境

2.3.3自定义shiro会话管理器

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

3SaaS-HRM中的认证授权

3.1需求分析

3.2搭建环境

3.2.2配置值对象

3.2.3配置未认证controller

3.2.4自定义realm授权

3.3.5 自定义会话管理器

3.3用户认证

 3.3.2shiro认证

3.4用户授权

3.5配置


项目亮点

1.权限开发:jwt,shiro

2.企业报表的解决方法:poi(xls) ,jasper(pdf)

3.代码生成工具的制作与解析

4.企业工作流定制:activiti7

5.人工智能:人脸登录

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

学习目标:
  • 理解SaaS的基本概念
  • 了解SAAS-HRM的基本需求和开发方式
  • 掌握Power Designer的用例图
  • 完成SAAS-HRM父模块及公共模块的环境搭建
  • 完成企业微服务中企业CRUD功能

1 初识SaaS

1.1 云服务的三种模式

1.1.1 IaaS(Infrastructure as a Service(基础设施即服务)

只提供基础设备,例如服务器(cpu、内存、网络)

1.1.2 PaaSPlatform-as-a-Service(平台即服务)

包含服务器、操作系统、数据库、运行环境库

1.1.3 SaaSSoftware-as-a-Service(软件即服务)

包含服务器、操作系统、数据库、运行环境库、应用

4 工程搭建

4.1 前置知识点的说明

Saas-HRM 系统后端采用
SpringBoot+SpringCloud+SpringMVC+SpringData
Saas-HRM 系统前端采用
基于 nodejs vue 框架完成编写使用 element-ui 组件库快速开发前端界面

4.2 开发环境要求

4.2.1 lombok 插件

1 idea 中安装插件

企业级saas模式hrm系统_第1张图片

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

    org.projectlombok
    lombok
    1.16.16
3 )常见注解
  • @Data 注解在类上;提供类所有属性的 getting setting 方法,此外还提供了equalscanEqual、hashCodetoString 方法
  • @Setter :注解在属性上;为属性提供 setting 方法
  • @Getter :注解在属性上;为属性提供 getting 方法
  • @NoArgsConstructor :注解在类上;为类提供一个无参的构造方法
  • @AllArgsConstructor :注解在类上;为类提供一个全参的构造方法

4.3 构建父工程

1.选择maven,因为父工程不需要什么骨架,直接next创建

2.应为父工程没有代码,所有把src目录删掉

IDEA 中创建父工程 ihrm_parent 并导入相应的坐标如下:
	pom
    ihrm_parent
    IHRM-黑马程序员
	
	
        org.springframework.boot
        spring-boot-starter-parent
        2.0.5.RELEASE
        
    

    
        UTF-8
        UTF-8
        1.8
        1.2.47
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-logging
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            com.alibaba
            fastjson
            ${fastjson.version}
        
        
            org.projectlombok
            lombok
            1.16.16
        
    

    
        
            spring-snapshots
            Spring Snapshots
            https://repo.spring.io/snapshot
            
                true
            
        
        
            spring-milestones
            Spring Milestones
            https://repo.spring.io/milestone
            
                false
            
        
    

    
        
            spring-snapshots
            Spring Snapshots
            https://repo.spring.io/snapshot
            
                true
            
        
        
            spring-milestones
            Spring Milestones
            https://repo.spring.io/milestone
            
                false
            
        
    
    
        
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.1
                
                    ${java.version}
                    ${java.version}
                
            

            
            
                org.apache.maven.plugins
                maven-surefire-plugin
                2.12.4
                
                    true
                
            
        
    

4.4 构建公共子模块

1.返回结果的实体类

2.分布式id生成器

疑问:问什么有了Result还要ResultCode枚举类呢?

其实只要Result也行,不过我们对于多条信息要不断的去get和set值,比较麻烦,所有定义ResultCode枚举类封装信息会比较方便

4.4.1 构建公共子模块ihrm-common

企业级saas模式hrm系统_第2张图片

4.4.2 创建返回结果实体类

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

package com.ihrm.common.entity;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


@Data
@NoArgsConstructor
public class Result {

    private boolean success;//是否成功
    private Integer code;// 返回码
    private String message;//返回信息
    private Object data;// 返回数据

    public Result(ResultCode code) {
        this.success = code.success;
        this.code = code.code;
        this.message = code.message;
    }

    public Result(ResultCode code,Object data) {
        this.success = code.success;
        this.code = code.code;
        this.message = code.message;
        this.data = data;
    }

    public Result(Integer code,String message,boolean success) {
        this.code = code;
        this.message = message;
        this.success = success;
    }

    public static Result SUCCESS(){
        return new Result(ResultCode.SUCCESS);
    }

    public static Result ERROR(){
        return new Result(ResultCode.SERVER_ERROR);
    }

    public static Result FAIL(){
        return new Result(ResultCode.FAIL);
    }
}

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

package com.ihrm.common.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult {
    private Long total;
    private List rows;
}

4.4.3 返回码定义类 (是枚举类型)

package com.ihrm.common.entity;

public enum ResultCode {

    SUCCESS(true,10000,"操作成功!"),
    //---系统错误返回码-----
    FAIL(false,10001,"操作失败"),
    UNAUTHENTICATED(false,10002,"您还未登录"),
    UNAUTHORISE(false,10003,"权限不足"),
    SERVER_ERROR(false,99999,"抱歉,系统繁忙,请稍后重试!");

    //---用户操作返回码----
    //---企业操作返回码----
    //---权限操作返回码----
    //---其他操作返回码----

    //操作是否成功
    boolean success;
    //操作代码
    int code;
    //提示信息
    String message;

    ResultCode(boolean success,int code, String message){
        this.success = success;
        this.code = code;
        this.message = message;
    }

    public boolean success() {
        return success;
    }

    public int code() {
        return code;
    }

    public String message() {
        return message;
    }

}

4.4.4 分布式ID生成器

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

主键id生成

        方案一:数据库自增(微服务架构不适合,如果对user表合并就会产生冲突,不推荐

        方案二:uuid全球唯一(缺点:太长32位,数据量太大,无序的

        方案三:借助全局redis(缺点:网络开销太大)

        方案四:雪花算法

snowflflake (雪花)算法。
不同毫秒-》不同机器-》2的12次方,同一毫秒,同一机器,最大支持4096个id->如果超过则等一毫秒
企业级saas模式hrm系统_第3张图片

代码是官方开源:官方下载即用

package com.leyou.common.utils;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;

//雪花算法代码实现
public class IdWorker {
    // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
    private final static long twepoch = 1288834974657L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
    // 机器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒内自增位
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生产id时间戳 */
    private static long lastTimestamp = -1L;
    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    // 数据标识id部分
    private final long datacenterId;

    public IdWorker(){
        this.datacenterId = getDatacenterId(maxDatacenterId);
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }
    /**
     * @param workerId
     *            工作机器ID
     * @param datacenterId
     *            序列号
     */
    public IdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    /**
     * 获取下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;

        return nextId;
    }

    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * 

* 获取 maxWorkerId *

*/ protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) { StringBuffer mpid = new StringBuffer(); mpid.append(datacenterId); String name = ManagementFactory.getRuntimeMXBean().getName(); if (!name.isEmpty()) { /* * GET jvmPid */ mpid.append(name.split("@")[0]); } /* * MAC + PID 的 hashcode 获取16个低位 */ return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1); } /** *

* 数据标识id部分 *

*/ protected static long getDatacenterId(long maxDatacenterId) { long id = 0L; try { InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = NetworkInterface.getByInetAddress(ip); if (network == null) { id = 1L; } else { byte[] mac = network.getHardwareAddress(); id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6; id = id % (maxDatacenterId + 1); } } catch (Exception e) { System.out.println(" getDatacenterId: " + e.getMessage()); } return id; } }

 4.5 搭建公共的实体类模块

1 )构建公共子模块 ihrm_common_model

企业级saas模式hrm系统_第4张图片

 2)引入坐标

    
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            org.example
            ihrm_common
            1.0-SNAPSHOT
        
    

5 企业微服务-企业CRUD

5.1 模块搭建

1 )搭建企业微服务模块 ihrm_company, pom.xml 引入依赖
    
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            mysql
            mysql-connector-java
        
        
            org.example
            ihrm_common
            1.0-SNAPSHOT
        
    
2 )添加配置文件 application.yml
        #服务配置
        #spring配置
                #应用配置
                # 数据库连接池
                # jpa配置
server:
 port: 9001
spring:
 application:
   name: ihrm-company #指定服务名
 datasource:
   driver-class-name: com.mysql.jdbc.Driver
   url: jdbc:mysql://localhost:3306/ihrm?useUnicode=true&characterEncoding=utf8
   username: root
   password: 111111
 jpa:
   database: MySQL
   show-sql: true
   open-in-view: true
3 )配置启动类
springboot的包扫描
jpa注解的扫描
package com.ihrm.company;

import com.ihrm.common.utils.IdWorker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;

//1.配置springboot的包扫描
@SpringBootApplication(scanBasePackages = "com.ihrm")
//2.配置jpa注解的扫描
@EntityScan(value="com.ihrm.domain.company")
public class CompanyApplication {

    /**
     * 启动方法
     */
    public static void main(String[] args) {
        SpringApplication.run(CompanyApplication.class,args);
    }

    @Bean
    public IdWorker idWorker() {
        return new IdWorker();
    }
}

5.2 企业管理-CRUD

5.2.1 表结构分析

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

5.2.2 完成企业增删改查操作

1、实体类

package com.ihrm.domain.company;

import lombok.*;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;

/**
 * 实体类代码:
 *  属性
 *  构造方法
 *  getter,setter方法
 *
 * lombok 插件 : 使用注解的形式替换getter setter,构造方法
 *      如何使用插件
 *          1.安装插件(在工程中引入响应的插件坐标即可)
 *                  
                        org.projectlombok
                        lombok
                        1.16.16
                    
 *          2.使用注解配置
 *                 配置到实体类上
 *                 @setter      : setter方法
 *                 @getter      :getter方法
 *                 @NoArgsConstructor   无参构造
 *                 @AllArgsConstructor  满参构造
 *                 @Data        : setter,getter,构造方法
 *
 * 使用jpa操作数据
 *      配置实体类和数据库表的映射关系:jpa注解
 *      1.实体类和表的映射关系
 *      2.字段和属性的映射关系
 *          i。主键属性的映射
 *          ii。普通属性的映射
 */
@Entity
@Table(name = "co_company")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Company implements Serializable {
    private static final long serialVersionUID = 594829320797158219L;
    //ID
    @Id
    private String id;
    /**
     * 公司名称
     */
    private String name;
    /**
     * 企业登录账号ID
     */
    private String managerId;
    /**
     * 当前版本
     */
    private String version;
    /**
     * 续期时间
     */
    private Date renewalDate;
    /**
     * 到期时间
     */
    private Date expirationDate;
    /**
     * 公司地区
     */
    private String companyArea;
    /**
     * 公司地址
     */
    private String companyAddress;
    /**
     * 营业执照-图片ID
     */
    private String businessLicenseId;
    /**
     * 法人代表
     */
    private String legalRepresentative;
    /**
     * 公司电话
     */
    private String companyPhone;
    /**
     * 邮箱
     */
    private String mailbox;
    /**
     * 公司规模
     */
    private String companySize;
    /**
     * 所属行业
     */
    private String industry;
    /**
     * 备注
     */
    private String remarks;
    /**
     * 审核状态
     */
    private String auditState;
    /**
     * 状态
     */
    private Integer state;
    /**
     * 当前余额
     */
    private Double balance;
    /**
     * 创建时间
     */
    private Date createTime;
}

2、DAO层

package com.ihrm.company.dao;

import com.ihrm.domain.company.Company;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

/**
 * 自定义dao接口继承
 *      JpaRepository<实体类,主键>
 *      JpaSpecificationExecutor<实体类>
 */
public interface CompanyDao extends JpaRepository ,JpaSpecificationExecutor {
}

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

package com.ihrm.company.service;

import com.ihrm.common.utils.IdWorker;
import com.ihrm.company.dao.CompanyDao;
import com.ihrm.domain.company.Company;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CompanyService {
    @Autowired
    private CompanyDao companyDao;

    @Autowired
    private IdWorker idWorker;
    /**
     * 保存企业
     *  1.配置idwork到工程
     *  2.在service中注入idwork
     *  3.通过idwork生成id
     *  4.保存企业
     */
    public void add(Company company) {
        //基本属性的设置
        String id = idWorker.nextId()+"";
        company.setId(id);
        //默认的状态
        company.setAuditState("0");//0:未审核,1:已审核
        company.setState(1); //0.未激活,1:已激活
        companyDao.save(company);
    }

    /**
     * 更新企业
     *  1.参数:Company
     *  2.根据id查询企业对象
     *  3.设置修改的属性
     *  4.调用dao完成更新
     */
    public void update(Company company) {
        Company temp = companyDao.findById(company.getId()).get();
        temp.setName(company.getName());
        temp.setCompanyPhone(company.getCompanyPhone());
        companyDao.save(temp);
    }


    /**
     * 删除企业
     */
    public void deleteById(String id) {
        companyDao.deleteById(id);
    }

    /**
     * 根据id查询企业
     */
    public Company findById(String id) {
        return companyDao.findById(id).get();
    }

    /**
     * 查询企业列表
     */
    public List findAll() {
        return companyDao.findAll();
    }
}

4、Controller层

package com.ihrm.company.controller;

import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.common.exception.CommonException;
import com.ihrm.company.service.CompanyService;
import com.ihrm.domain.company.Company;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

//解决跨域问题
@CrossOrigin
@RestController
@RequestMapping(value="/company")
public class CompanyController {

    @Autowired
    private CompanyService companyService;

    //保存企业
    @RequestMapping(value="",method = RequestMethod.POST)
    public Result save(@RequestBody Company company)  {
        //业务操作
        companyService.add(company);
        return new Result(ResultCode.SUCCESS);
    }

    //根据id更新企业
    /**
     * 1.方法
     * 2.请求参数
     * 3.响应
     */
    @RequestMapping(value = "/{id}",method = RequestMethod.PUT)
    public Result update(@PathVariable(value="id") String id, @RequestBody Company company ) {
        //业务操作
        company.setId(id);
        companyService.update(company);
        return new Result(ResultCode.SUCCESS);
    }

    //根据id删除企业
    @RequestMapping(value="/{id}",method = RequestMethod.DELETE)
    public Result delete(@PathVariable(value="id") String id) {
        companyService.deleteById(id);
        return new Result(ResultCode.SUCCESS);
    }

    //根据id查询企业
    @RequestMapping(value="/{id}",method = RequestMethod.GET)
    public Result findById(@PathVariable(value="id") String id) throws CommonException {
        Company company = companyService.findById(id);
        Result result = new Result(ResultCode.SUCCESS);
        result.setData(company);
        return result;
    }

    //查询全部企业列表
    @RequestMapping(value="",method = RequestMethod.GET)
    public Result findAll() {
        List list = companyService.findAll();
        Result result = new Result(ResultCode.SUCCESS);
        result.setData(list);
        return result;
    }
}

5.3公共异常处理

为了使我们的代码更容易维护,同时给用户最好的用户体验,有必要对系统中可能出现的异常进行处理。spring提供了@ControllerAdvice注解和@ExceptionHandler可以很好的在控制层对异常进行统一处理

@ControllerAdvice:全局控制器异常处理,何控制器抛出异常时,注解这个的类可以拦截并处理所抛出的异常

@ExceptionHandler:定义了该方法处理的特定异常类型
(1)添加自定义的异常

package com.ihrm.common.exception;
import com.ihrm.common.entity.ResultCode;
import lombok.Getter;
@Getter
public class CommonException extends RuntimeException {
    private static final long serialVersionUID = 1L;//序列化和反序列化操作的版本固定版本
    private ResultCode code = ResultCode.SERVER_ERROR;//定义结果集中一个枚举作为作为一个对象
    public CommonException(){}
    public CommonException(ResultCode resultCode) {
        super(resultCode.message());
        this.code = resultCode; //枚举对象=客户传入的的报错对象
    }
}

(2)配置公共异常处理

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

5.4跨域处理

跨域是什么?浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 。我们是采用前后端分离开发的,也是前后端分离部署的,必然会存在跨域问题。 怎么解决跨域?很简单,

只需要在controller类上添加注解@CrossOrigin 即可

!这个注解其实是CORS的实现。 CORS(Cross-Origin Resource Sharing, 跨源资源共享)是W3C出的一个标准,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。因此,要想实现CORS进行跨域,需要服务器进行一些设置,同时前 端也需要做一些配置和分析。本文简单的对服务端的配置和前端的一些设置进行分析。

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

知识点:

理解多租户的数据库设计方案
熟练使用PowerDesigner构建数据库模型
理解前端工程的基本架构和执行流程
完成前端工程企业模块开发

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

1.1 多租户是什么

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

1.2 需求分析

        传统软件模式,指将软件产品进行买卖,是一种单纯的买卖关系,客户通过买断的方式获取软件的使用权,软件的源码属于客户所有,因此传统软件是部署到企业内部,不同的企业各自部署一套自己的软件系统
       Saas模式,指服务提供商提供的一种软件服务,应用统一部署到服务提供商的服务器上,客户可以根据自己的实际需求按需付费。用户购买基于WEB的软件,而不是将软件安装在自己的电脑上,用户也无需对软件进行定期的维护与管理

企业级saas模式hrm系统_第5张图片

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

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

目前基于多租户的数据库设计方案通常有如下三种:
        独立数据库:隔离,安全,易扩展
        共享数据库、独立 Schema:有第一定的隔离性,成本低
        共享数据库、共享数据表(使用这一套):成本最低,设计比较复杂,隔离性要求较高

1.3.1 独立数据库

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

1.3.2 共享数据库、独立 Schema

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

企业级saas模式hrm系统_第6张图片

 mysql数据库:mysql数据中的schema比较特殊,并不是数据库的下一级,而是等同于数据库。比如执行create schema test 和执行create database test效果是一模一样的。

企业级saas模式hrm系统_第7张图片

 共享数据库、独立 Schema:即多个或所有的租户使用同一个数据库服务(如常见的ORACLE或MYSQL数据库),但是每个租户一个Schema。
       优点: 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
       缺点: 如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据; 如果需要跨租户统计数据,存在一定困难。
这种方案是方案一的变种。只需要安装一份数据库服务,通过不同的Schema对不同租户的数据进行隔离。由于数据库服务是共享的,所以成本相对低廉。

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

共享数据库、共享数据表:即租户共享同一个Database,同一套数据库表(所有租户的数据都存放在一个数据库的同一套表中)。在表中增加租户ID等租户标志字段,表明该记录是属于哪个租户的。
       优点:所有租户使用同一套数据库,所以成本低廉。
       缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量,数据备份和恢复最困难。
这种方案和基于传统应用的数据库设计并没有任何区别,但是由于所有租户使用相同的数据库表,所以需要做好对
每个租户数据的隔离安全性处理,这就增加了系统设计和数据管理方面的复杂程度。
企业级saas模式hrm系统_第8张图片

1.4 SAAS-HRM数据库设计

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

2 数据库设计与建模

2.1 数据库设计的三范式

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

       例如:假设我们的表中有一列是地址,里面存的值是诸如:中国北京。那么这样就违反了第一范式,因为中国北京其实可以很好的拆分为中国和北京两个,然后数据库里面可以出现两列:国籍和城市。这样才是符合第一范式的。

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

      假如:我们有一个学生表,里面存的是用户名,密码等,如果再加上 英语成绩,数学成绩等字段,那么就违反了第二范式。因为这样显得学生表不伦不类,不知道到底要存什么样的数据,所以为了满足第二范式,就应该再创建一张成绩表。

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

       例如:创建一个订单表,有订单单价,订单个数,订单总计三个字段,那么这就违反了第三范式,因为总计这列的值完全可以通过单价乘以个数得到,不需要额外去存储。还有一个例子,我们有一个员工表,里面存了员工信息,还有和公司关联的company_id和company_name字段,同样也违反了第三范式,因为我们只要存了company_id,就可以查询企业表,从而得到company_name。

以上说的三范式,出现的年代比较久远了,那个时候服务器的存储的成本还比较高,也就是硬盘还比较贵,所以为了节省硬盘,就应该尽量减少硬盘的使用空间。而现在硬盘已经不是昂贵的东西了,所以就出现了反三范式:

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

        就拿上面的第三范式的例子来说,如果我们遵守了第三范式,没有存储总计的值,那么如果我们要做统计的时候,每次都要去计算单价乘以个数来得到总计,如果表中有十万数据,就需要计算十万次,这样势必会降低效率。而反三范式就是通过冗余字段,来提高效率,只需要通过查询就可以得到结果,无需再次去逻辑运算,这也就是达到了以空间换时间的目的。

2.2 数据库建模

它主要包括两部分内容:基本的数据结构;对约束建模(外键、组建)。

2.2.1 建模工具

PowerDesigner:他的优势在于:不用使用create table等语句创建表结构,数据库设计人员只关注如何进行数据建模即可,将来的数据库语句,可以自动生成。

2.2.2 使用pd建模

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

企业级saas模式hrm系统_第9张图片

 2. 控制面板

企业级saas模式hrm系统_第10张图片

3. 创建数据库表

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

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

企业级saas模式hrm系统_第11张图片

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

企业级saas模式hrm系统_第12张图片

企业级saas模式hrm系统_第13张图片

企业级saas模式hrm系统_第14张图片

4、导出sql语句

我们之前做的这些操作,都可以进行sql的导出,然后在数据库中执行即可:

菜单栏:Databse——》Genarate Database:

企业级saas模式hrm系统_第15张图片

 生成的sql文件内容如下:

/*==============================================================*/
/* DBMS name:      MySQL 5.0                                    */
/* Created on:     2019/8/11 14:13:28                           */
/*==============================================================*/
 
 
drop table if exists co_company;
 
drop table if exists co_dept;
 
/*==============================================================*/
/* Table: co_company                                            */
/*==============================================================*/
create table co_company
(
   id                   varchar(40) not null,
   name                 varchar(200),
   company_area         varchar(200),
   primary key (id)
);
 
/*==============================================================*/
/* Table: co_dept                                               */
/*==============================================================*/
create table co_dept
(
   id                   varchar(40) not null,
   name                 varchar(400),
   company_id           varchar(40),
   primary key (id)
);
 
alter table co_dept add constraint FK_Reference_1 foreign key (company_id)
      references co_company (id) on delete restrict on update restrict;

第四章 权限管理与jwt鉴权

4 常见的认证机制

4.1 HTTP Basic Auth

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

4.2 Cookie Auth

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

4.3 OAuth

        OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一 web 服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 OAuth 允许用户提供一个令牌,而 不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频 编辑网站 ) 在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样, OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
企业级saas模式hrm系统_第16张图片

 这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证 权限管理的企业应用。

4.4 Token Auth

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

企业级saas模式hrm系统_第17张图片

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

5 HRM中的TOKEN签发与验证

5.1 什么是JWT

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

5.2 JJWT的快速入门

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

        
            io.jsonwebtoken
            jjwt
            0.6.0
        
2 )创建类 CreateJwtTest ,用于生成 token
    /**
     * 通过jjwt创建token
     */
    public static void main(String[] args) {
        JwtBuilder jwtBuilder = Jwts.builder().setId("88").setSubject("小白")
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256, "ihrm")
                .claim("companyId","123456")
                .claim("companyName","小新股份有限公司")
                ;
        String token = jwtBuilder.compact();
        System.out.println(token);
    }
3 )测试运行,输出如下 :

5.2.2 token的解析

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

    /**
     * 解析jwtToken字符串
     */
    public static void main(String[] args) {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4OCIsInN1YiI6IuWwj-eZvSIsImlhdCI6MTU0MzMwODM1NiwiY29tcGFueUlkIjoiMTIzNDU2IiwiY29tcGFueU5hbWUiOiLmsZ_oi4_kvKDmmbrmkq3lrqLmlZnogrLogqHku73mnInpmZDlhazlj7gifQ.lacFfiWnBkbCQuHIwsB-S7gRkXxesTx8GOhhtIjALLI";
        Claims claims = Jwts.parser().setSigningKey("ihrm").parseClaimsJws(token).getBody();

        //私有数据存放在claims
        System.out.println(claims.getId());
        System.out.println( claims.getSubject());
        System.out.println(claims.getIssuedAt());

        //解析自定义claim中的内容
        String companyId = (String)claims.get("companyId");
        String companyName = (String)claims.get("companyName");

        System.out.println(companyId + "---" + companyName);
    }
}
试着将 token 或签名秘钥篡改一下,会发现运行时就会报错,所以解析 token 也就是验证 token

5.3 JWT工具类

ihrm_common工程中创建JwtUtil工具类

不可以jwtBuilder.setClaim(map),被覆盖其他的

@Getter
@Setter
@ConfigurationProperties("jwt.config")
public class JwtUtils {
    //签名私钥
    private String key;
    //签名的失效时间
    private Long ttl;

    /**
     * 设置认证token
     *      id:登录用户id
     *      subject:登录用户名
     *
     */
    public String createJwt(String id, String name, Map map) {
        //1.设置失效时间
        long now = System.currentTimeMillis();//当前毫秒
        long exp = now + ttl;
        //2.创建jwtBuilder
        JwtBuilder jwtBuilder = Jwts.builder().setId(id).setSubject(name)
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256, key);
        //3、直接setClaim(map)会把上边的都覆盖掉
        //jwtBuilder.setClaim(map)
        //3.根据map设置claims
        for(Map.Entry entry : map.entrySet()) {
            jwtBuilder.claim(entry.getKey(),entry.getValue());
        }
        jwtBuilder.setExpiration(new Date(exp));
        //4.创建token
        String token = jwtBuilder.compact();
        return token;
    }


    /**
     * 解析token字符串获取clamis
     */
    public Claims parseJwt(String token) {
        Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
        return claims;
    }

}
3 )修改 ihrm_common 工程的 application.yml, 添加配置
jwt:
 config:
    key: saas-ihrm
    ttl: 360000

5.4 登录成功签发token

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

@Bean    
public JwtUtil jwtUtil(){    
 return new util.JwtUtil();        
}
2 )添加登录方法
    /**
     * 用户登录
     *  1.通过service根据mobile查询用户
     *  2.比较password
     *  3.生成jwt信息
     *
     */
    @RequestMapping(value="/login",method = RequestMethod.POST)
    public Result login(@RequestBody Map loginMap) {
        String mobile = loginMap.get("mobile");
        String password = loginMap.get("password");
        User user = userService.findByMobile(mobile);
        //登录失败
        if(user == null || !user.getPassword().equals(password)) {
            return new Result(ResultCode.MOBILEORPASSWORDERROR);
        }else {
        //登录成功
            Map map = new HashMap<>();
            map.put("companyId",user.getCompanyId());
            map.put("companyName",user.getCompanyName());
            String token = jwtUtils.createJwt(user.getId(), user.getUsername(), map);
            return new Result(ResultCode.SUCCESS,token);
        }
    }

5.5 获取用户信息鉴权

需求:用户登录成功之后,会发送一个新的请求到服务端,获取用户的详细信息。获取用户信息的过程中必须登录才能,否则不能获取。
前后端约定:前端请求微服务时需要添加头信息 Authorization , 内容为 Bearer+ 空格 +token
1 )添加响应值对象
@Setter
@Getter
public class ProfileResult {
    private String mobile;
    private String username;
    private String company;
    private Map roles = new HashMap<>();

    public ProfileResult(User user) {
        this.mobile = user.getMobile();
        this.username = user.getUsername();
        this.company = user.getCompanyName();

        Set roles = user.getRoles();
        Set menus = new HashSet<>();
        Set points = new HashSet<>();
        Set apis = new HashSet<>();
        for (Role role : roles) {
            Set perms = role.getPermissions();
            for (Permission perm : perms) {
                String code = perm.getCode();
                if(perm.getType() == 1) {
                    menus.add(code);
                }else if(perm.getType() == 2) {
                    points.add(code);
                }else {
                    apis.add(code);
                }
            }
        }

        this.roles.put("menus",menus);
        this.roles.put("points",points);
        this.roles.put("apis",apis);
    }
}
2 )添加 profifile 方法
    /**
     * 用户登录成功之后,获取用户信息
     *      1.获取用户id
     *      2.根据用户id查询用户
     *      3.构建返回值对象
     *      4.响应
     */
    @RequestMapping(value="/profile",method = RequestMethod.POST)
    public Result profile(HttpServletRequest request) throws Exception {

        /**
         * 从请求头信息中获取token数据
         *   1.获取请求头信息:名称=Authorization
         *   2.替换Bearer+空格
         *   3.解析token
         *   4.获取clamis
         */
        //1.获取请求头信息:名称=Authorization
        String authorization = request.getHeader("Authorization");
        if(StringUtils.isEmpty(authorization)) {
            throw new CommonException(ResultCode.UNAUTHENTICATED);
        }
        //2.替换Bearer+空格
        String token = authorization.replace("Bearer ","");
        //3.解析token
        Claims claims = jwtUtils.parseJwt(token);
        String userid = claims.getId();
        User user = userService.findById(userid);
        ProfileResult result = new ProfileResult(user);
        return new Result(ResultCode.SUCCESS,result);
    }

3 前端框架

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

技术栈
      vue 2.5++
      elementUI 2.2.2
      vuex
      axios
      vue-router
      vue-i18n
前端环境
      node 8.++
      npm 5.++

3.2 启动与安装

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

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

3.3 工程结构

整个前端工程的工程目录结构如下:

企业级saas模式hrm系统_第18张图片

 企业级saas模式hrm系统_第19张图片

3.4 执行流程分析

3.4.1 路由和菜单

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

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

企业级saas模式hrm系统_第20张图片

3.4.2 前端数据交互

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

1. UI 组件交互操作;
2. 调用统一管理的 api service 请求函数;
3. 使用封装的 request.js 发送请求;
4. 获取服务端返回;
5. 更新 data;

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

api/
       frame.js
       menus.js
       users.js
       permissions.js
       ...

4 企业管理

4.1 需求分析

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

4.2 搭建环境

4.2.1 新增模块

(1)手动创建
方式一:在src目录下创建文件夹,命名规则:module-模块名称()
在文件夹下按照指定的结构配置assets,components,pages,router,store等文件
(2)使用命令自动创建
安装命令行工具:npm install -g itheima-cli
执行命令:itheima moduleAdd saas-clients         `saas-clients` 是新模块的名字

自动创建这些目录和文件
│ ├── module-saas-clients | saas-clients模块主目录
│ │ ├── assets | 资源
│ │ ├── components | 组件
│ │ ├── pages | 页面
│ │ │ └── index.vue | 示例
│ │ ├── router | 路由
│ │ │ └── index.js | 示例
│ │ └── store | 数据
│ │ └── app.js | 示例

每个模块所有的素材、页面、组件、路由、数据,都是独立的,方便大型项目管理,
在实际项目中会有很多子业务项目,它们之间的关系是平行的、低耦合、互不依赖。

注意:创建完模块之后,导致名称和demo模块一样,所以需要修改module-demo/router下面的index.js:
企业级saas模式hrm系统_第21张图片

4.2.2 构造模拟数据

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

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

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

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

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

4.2.3 注册模块

编辑 src/main.js

/*
* 注册 - 业务模块
*/
import dashboard from '@/module-dashboard/' // 面板
import demo from '@/module-demo/' // 面板
 
import saasClients from '@/module-saas-clients/' //刚新添加的 企业管理
 
import tools from './utils/common.js'
Vue.prototype.$tools = tools
 
Vue.use(tools)
Vue.use(dashboard, store)
Vue.use(demo, store)
Vue.use(saasClients, store)  ///注册  刚新添加的 企业管理

4.2.4 配置路由菜单

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

/*
 * @Author: dongwen.zeng <[email protected]> 
 * @Description: xxx业务模块 
 * @Date: 2018-04-13 16:13:27 
 * @Last Modified by: hans.taozhiwei
 * @Last Modified time: 2018-09-03 11:12:47
 */
 
import Layout from '@/module-dashboard/pages/layout'
const _import = require('@/router/import_' + process.env.NODE_ENV)
 
export default [
  {
    root: true,
    path: '/saas-clients',
    component: Layout,
    redirect: 'noredirect',
    name: 'saas-clients',
    meta: {
      title: 'xxx业务模块管理',
      icon: 'international'
    },
    children: [
      {
        path: 'index',
        component: _import('saas-clients/pages/index'),
        name: 'saas-clients-index',
        meta: {title: 'SaaS企业管理', icon: 'international', noCache: true}
      }
    ]
  }
]

4.2.5 编写业务页面

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


 

注意文件名 驼峰格式 首字小写
页面请放在目录 /src/module-saas-clients/pages/
组件请放在目录 /src/module-saas-clients/components/
页面路由请修改 /src/module-saas-clients/router/index.js

4.3 企业操作

4.3.1 创建api

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

import {createAPI, createFormAPI} from '@/utils/request'  //导入相关工具类,框架自己提供的
//第一个参数/company是请求路径(路径可以是完全路径,也可以是部分路径,因为我们在config/dev.env.js下面有前缀配置),
//  BASE_API: '"http://localhost:9001/"'     第二参数是请求方式,第三个参数请求的参数数据
export const list = data => createAPI('/company', 'get', data)
// data代表请求的对象,${data.id}表示从请求的对象中取出id属性
export const detail = data => createAPI(`/company/${data.id}`, 'get', data)

4.3.2 企业列表

想要显示序号只需要吧prop改为type="index"


 

4.3.3 企业详情

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

{
     path: 'details/:id',  //特别注意这个路径的写法
     component: _import('saas-clients/pages/details'),
     name: 'saas-clients-details',
     meta: {title: 'saas企业详情', icon: 'component', noCache: true}
}

(2)完成详情展示

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

  // 钩子函数获取地址参数
  created() {
    var id = this.$route.params.id  //获取路径上的参数id的值
    this.details(id);  
  },

//Switch开关


 

 

4.4 与后台对接测试

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

(2)注释掉src/mock目录下index.js下面的:

//配置模拟数据接口
//Mock.mock(/\/company\/+/, 'get', CompanyAPI.sassDetail)//根据id查询
//Mock.mock(/\/company/, 'get', CompanyAPI.list) //访问企业列表

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

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

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

学习目标:


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

1 组织机构管理

1.1 需求分析

1.1.1 需求分析

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

企业级saas模式hrm系统_第22张图片

 1.1.2 数据库表设计

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

1.2 微服务实现

1.2.1 抽取公共代码

(1) 在公共controller

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

@ModelAttribute  //在所有controller层前之前执行的方法
import org.springframework.web.bind.annotation.ModelAttribute;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 公共controller
* 获取request,response
* 获取企业id,获取企业名称
*/
public class BaseController {

    protected HttpServletRequest request;
    protected HttpServletResponse response;
    protected String companyId;  //暂定假设值
    protected  String  companyName;

    @ModelAttribute  //在所有controller层前之前执行的方法
    public void setResAnReq(HttpServletRequest request,HttpServletResponse response) {
        this.request = request;
        this.response = response;
        /**
         * 目前使用 companyId = 1
         *         companyName = "传智播客"
         */
      
    }
    
    //企业id,(暂时使用1,以后会动态获取)
    public String parseCompanyId() {
      return "1";
    }
     public String parseCompanyName() {
      return "江苏传智播客教育股份有限公司";
}

}

(2) 公共service

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

    
        
            org.springframework.data
            spring-data-jpa
        
        
            org.hibernate.javax.persistence
            hibernate-jpa-2.1-api
         
    
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

public class BaseService {

    protected Specification getSpec(String companyId) {
        Specification spect = new Specification() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder cb) {
                //根据企业id查询
                return cb.equal(root.get("companyId").as(String.class),companyId);
            }
        };
        return spect;
    }
}

1.2.2 实现基本CRUD操作

(1)实体类

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

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import java.io.Serializable;
import java.util.Date;
import java.util.List;

/**
 * (Department)实体类
 */
@Entity
@Table(name = "co_department")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department implements Serializable {
    private static final long serialVersionUID = -9084332495284489553L;
    //ID
    @Id
    private String id;
    /**
     * 父级ID
     */
    private String pid;
    /**
     * 企业ID
     */
    private String companyId;
    /**
     * 部门名称
     */
    private String name;
    /**
     * 部门编码,同级部门不可重复
     */
    private String code;

    /**
     * 负责人ID
     */
    private String managerId;
    /**
    *  负责人名称
    */
    private String manager;

    /**
     * 介绍
     */
    private String introduce;
    /**
     * 创建时间
     */
    private Date createTime;
}

(2)持久化层

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

import com.ihrm.domain.company.Department;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

/**
 * /**
* 部门操作持久层:参数类型:函数JpaRepository
                   参数一:对象+对象主键类型
                      函数JpaSpecificationExecutor
                    参数二:对象
*/
 */
public interface DepartmentDao extends JpaRepository ,JpaSpecificationExecutor {
}

(3)业务层

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

import com.ihrm.common.service.BaseService;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.company.dao.DepartmentDao;
import com.ihrm.domain.company.Department;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.List;

@Service
public class DepartmentService extends BaseService {

    @Autowired
    private DepartmentDao departmentDao;

    @Autowired
    private IdWorker idWorker;

    /**
     * 1.保存部门
     */
    public void save(Department department) {
        //设置主键的值
        String id = idWorker.nextId()+"";
        department.setId(id);
        //调用dao保存部门
        departmentDao.save(department);
    }

    /**
     * 2.更新部门
     */
    public void update(Department department) {
        //1.根据id查询部门
        Department dept = departmentDao.findById(department.getId()).get();
        //2.设置部门属性
        dept.setCode(department.getCode());
        dept.setIntroduce(department.getIntroduce());
        dept.setName(department.getName());
        //3.更新部门
        departmentDao.save(dept);
    }

    /**
     * 3.根据id查询部门
     */
    public Department findById(String id) {
        return departmentDao.findById(id).get();
    }

    /**
     * 4.查询全部部门列表
     */
    public List findAll(String companyId) {
        /**
         * 用户构造查询条件
         *      1.只查询companyId
         *      2.很多的地方都需要根据companyId查询
         *      3.很多的对象中都具有companyId
         *
         */
//        Specification spec = new Specification() {
//            /**
//             * 用户构造查询条件
//             *      root   :包含了所有的对象数据
//             *      cq     :一般不用
//             *      cb     :构造查询条件
//             */
//            public Predicate toPredicate(Root root, CriteriaQuery cq, CriteriaBuilder cb) {
//                //根据企业id查询
//                return cb.equal(root.get("companyId").as(String.class),companyId);
//            }
//        };
        return departmentDao.findAll(getSpec(companyId));
    }

    /**
     * 5.根据id删除部门
     */
    public void deleteById(String id) {
        departmentDao.deleteById(id);
    }
}

(4)控制层

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

package com.ihrm.company.controller;

import com.ihrm.common.controller.BaseController;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.company.service.CompanyService;
import com.ihrm.company.service.DepartmentService;
import com.ihrm.domain.company.Company;
import com.ihrm.domain.company.Department;
import com.ihrm.domain.company.response.DeptListResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

//1.解决跨域
@CrossOrigin
//2.声明restContoller
@RestController
//3.设置父路径
@RequestMapping(value="/company")   //  company/deparment
public class DepartmentController extends BaseController{

    @Autowired
    private DepartmentService departmentService;

    @Autowired
    private CompanyService companyService;
    /**
     * 保存
     */
    @RequestMapping(value="/department",method = RequestMethod.POST)
    public Result save(@RequestBody Department department) {
        //1.设置保存的企业id
        /**
         * 企业id:目前使用固定值1,以后会解决
         */
        department.setCompanyId(companyId);
        //2.调用service完成保存企业
        departmentService.save(department);
        //3.构造返回结果
        return new Result(ResultCode.SUCCESS);
    }

    /**
     * 查询企业的部门列表
     * 指定企业id
     */
    @RequestMapping(value="/department",method = RequestMethod.GET)
    public Result findAll() {
        //1.指定企业id
        Company company = companyService.findById(companyId);
        //2.完成查询
        List list = departmentService.findAll(companyId);
        //3.构造返回结果
        DeptListResult deptListResult = new DeptListResult(company,list);
        return new Result(ResultCode.SUCCESS,deptListResult);
    }

    /**
     * 根据ID查询department
     */
    @RequestMapping(value="/department/{id}",method = RequestMethod.GET)
    public Result findById(@PathVariable(value="id") String id) {
        Department department = departmentService.findById(id);
        return new Result(ResultCode.SUCCESS,department);
    }

    /**
     * 修改Department
     */
    @RequestMapping(value="/department/{id}",method = RequestMethod.PUT)
    public Result update(@PathVariable(value="id") String id,@RequestBody Department department) {
        //1.设置修改的部门id
        department.setId(id);
        //2.调用service更新
        departmentService.update(department);
        return new Result(ResultCode.SUCCESS);
    }

    /**
     * 根据id删除
     */
    @RequestMapping(value="/department/{id}",method = RequestMethod.DELETE)
    public Result delete(@PathVariable(value="id") String id) {
        departmentService.deleteById(id);
        return new Result(ResultCode.SUCCESS);
    }
}

创建返回值对象DeptListResult

package com.ihrm.domain.company.response;

import com.ihrm.domain.company.Company;
import com.ihrm.domain.company.Department;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;


import java.util.List;

@Getter
@Setter
@NoArgsConstructor
public class DeptListResult {

    private  String  companyId;  //交叉部分元素公司与部门交叉部分

    private  String  companyName;
    private  String  companyManage;


    private List depts;  //对象等于department

    public  DeptListResult(Company company, List depts){

        this.companyId=company.getId();

        this.companyName= company.getName();
        this.companyManage= company.getLegalRepresentative();  //公司联系人



    }

}

1.3 前端实现

1.3.1 创建模块

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

itheima moduleAdd departments1.

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

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

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

import Layout from '@/module-dashboard/pages/layout'
const _import = require('@/router/import_' + process.env.NODE_ENV)
export default [
 {
    root: true,
    path: '/departments',
    component: Layout,
    redirect: 'noredirect',
    name: 'departments',
    meta: {
      title: '组织架构管理',
      icon: 'architecture'
   },
    children: [
     {
        path: 'index',
        component: _import('departments/pages/index'),
        name: 'organizations-index',
        meta: {title: '组织架构', icon: 'architecture', noCache: true}
     }
   ]
 }

1.3.2 配置请求API

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

import {
createAPI, createFileAPI
} from '@/utils/request'
export const organList = data => createAPI('/company/departments', 'get', data)
export const add = data => createAPI('/company/departments', 'post', data)
export const update = data => createAPI(`/company/departments/${data.id}`, 'put', data)
export const detail = data => createAPI(`/company/departments/${data.id}`, 'get', data)
export const remove = data => createAPI(`/company/departments/${data.id}`, 'delete',
data)
export const changeDept = data => createAPI(`/company/departments/changeDept`, 'put',
data)
export const saveOrUpdate = data => {return data.id?update(data):add(data)}

课堂讲解的代码

import {createAPI} from '@/utils/request'

//查询部门列表
export const list = data => createAPI('/company/department', 'get', data)
//保存部门
//data  {id:“”,name:“”}
export const save = data => createAPI('/company/department', 'post', data)
//根据id查询部门 {id:“”}
export const find = data => createAPI(`/company/department/${data.id}`, 'get', data)
//根据id删除部门 {id:""}
export const deleteById = data => createAPI(`/company/department/${data.id}`, 'delete', data)
//根据id更新部门 {id:"",name:"",code:""}
export const update = data => createAPI(`/company/department/${data.id}`, 'put', data)
//保存或更新的方法
export const saveOrupdate = data => {return data.id?update(data):save(data)}


1.3.3 构造列表

(1)构造基本页面样式

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