目录
项目亮点
第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 完成企业增删改查操作
第二章数据库设计与前端框架
知识点:
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.人工智能:人脸登录
只提供基础设备,例如服务器(cpu、内存、网络)
包含服务器、操作系统、数据库、运行环境库
包含服务器、操作系统、数据库、运行环境库、应用
org.projectlombok
lombok
1.16.16
1.选择maven,因为父工程不需要什么骨架,直接next创建
2.应为父工程没有代码,所有把src目录删掉
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}
org.apache.maven.plugins
maven-surefire-plugin
2.12.4
true
1.返回结果的实体类
2.分布式id生成器
疑问:问什么有了Result还要ResultCode枚举类呢?
其实只要Result也行,不过我们对于多条信息要不断的去get和set值,比较麻烦,所有定义ResultCode枚举类封装信息会比较方便
(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;
}
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;
}
}
主键id生成
方案一:数据库自增(微服务架构不适合,如果对user表合并就会产生冲突,不推荐)
方案二:uuid全球唯一(缺点:太长32位,数据量太大,无序的)
方案三:借助全局redis(缺点:网络开销太大)
方案四:雪花算法
代码是官方开源:官方下载即用
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;
}
}
(2)引入坐标
org.springframework.boot
spring-boot-starter-data-jpa
org.example
ihrm_common
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
org.example
ihrm_common
1.0-SNAPSHOT
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
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();
}
}
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;
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;
}
}
为了使我们的代码更容易维护,同时给用户最好的用户体验,有必要对系统中可能出现的异常进行处理。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();
}
}
}
跨域是什么?浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 。我们是采用前后端分离开发的,也是前后端分离部署的,必然会存在跨域问题。 怎么解决跨域?很简单,
只需要在controller类上添加注解@CrossOrigin 即可
!这个注解其实是CORS的实现。 CORS(Cross-Origin Resource Sharing, 跨源资源共享)是W3C出的一个标准,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。因此,要想实现CORS进行跨域,需要服务器进行一些设置,同时前 端也需要做一些配置和分析。本文简单的对服务端的配置和前端的一些设置进行分析。
理解多租户的数据库设计方案
熟练使用PowerDesigner构建数据库模型
理解前端工程的基本架构和执行流程
完成前端工程企业模块开发
多租户技术(Multi-TenancyTechnology)又称多重租赁技术:是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重点就是同一套程序下实现多用户数据的隔离。
传统软件模式,指将软件产品进行买卖,是一种单纯的买卖关系,客户通过买断的方式获取软件的使用权,软件的源码属于客户所有,因此传统软件是部署到企业内部,不同的企业各自部署一套自己的软件系统
Saas模式,指服务提供商提供的一种软件服务,应用统一部署到服务提供商的服务器上,客户可以根据自己的实际需求按需付费。用户购买基于WEB的软件,而不是将软件安装在自己的电脑上,用户也无需对软件进行定期的维护与管理
在SaaS平台里需要使用共用的数据中心以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍可以保障客户的数据正常使用。由此带来了新的挑战,就是如何对应用数据进行设计,以支持多租户,而这种设计的思路,是要在数据的共享、安全隔离和性能间取得平衡。
目前基于多租户的数据库设计方案通常有如下三种:
独立数据库:隔离,安全,易扩展
共享数据库、独立 Schema:有第一定的隔离性,成本低
共享数据库、共享数据表(使用这一套):成本最低,设计比较复杂,隔离性要求较高
独立数据库:每个租户一个数据库。
优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。
(1) 什么是Schema
oracle数据库:在oracle中一个数据库可以具有多个用户,那么一个用户一般对应一个Schema,表都是建立在Schema中的,(可以简单的理解:在oracle中一个用户一套数据库表)
mysql数据库:mysql数据中的schema比较特殊,并不是数据库的下一级,而是等同于数据库。比如执行create schema test 和执行create database test效果是一模一样的。
共享数据库、独立 Schema:即多个或所有的租户使用同一个数据库服务(如常见的ORACLE或MYSQL数据库),但是每个租户一个Schema。
优点: 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可支持更多的租户数量。
缺点: 如果出现故障,数据恢复比较困难,因为恢复数据库将牵涉到其他租户的数据; 如果需要跨租户统计数据,存在一定困难。
这种方案是方案一的变种。只需要安装一份数据库服务,通过不同的Schema对不同租户的数据进行隔离。由于数据库服务是共享的,所以成本相对低廉。
共享数据库、共享数据表:即租户共享同一个Database,同一套数据库表(所有租户的数据都存放在一个数据库的同一套表中)。在表中增加租户ID等租户标志字段,表明该记录是属于哪个租户的。
优点:所有租户使用同一套数据库,所以成本低廉。
缺点:隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量,数据备份和恢复最困难。
这种方案和基于传统应用的数据库设计并没有任何区别,但是由于所有租户使用相同的数据库表,所以需要做好对
每个租户数据的隔离安全性处理,这就增加了系统设计和数据管理方面的复杂程度。
在SAAS-HRM平台中,分为了试用版和正式版。处于教学的目的,试用版采用共享数据库、共享数据表的方式设计。正式版采用基于mysql的共享数据库、独立 Schema设计(后续课程)。
1.第一范式(1NF):确保每一列的原子性(做到每列不可拆分)
例如:假设我们的表中有一列是地址,里面存的值是诸如:中国北京。那么这样就违反了第一范式,因为中国北京其实可以很好的拆分为中国和北京两个,然后数据库里面可以出现两列:国籍和城市。这样才是符合第一范式的。
2.第二范式(2NF):在第一范式的基础上,非主字段必须依赖于主字段(一个表只做一件事)
假如:我们有一个学生表,里面存的是用户名,密码等,如果再加上 英语成绩,数学成绩等字段,那么就违反了第二范式。因为这样显得学生表不伦不类,不知道到底要存什么样的数据,所以为了满足第二范式,就应该再创建一张成绩表。
3.第三范式(3NF):在第二范式的基础上,消除传递依赖。
例如:创建一个订单表,有订单单价,订单个数,订单总计三个字段,那么这就违反了第三范式,因为总计这列的值完全可以通过单价乘以个数得到,不需要额外去存储。还有一个例子,我们有一个员工表,里面存了员工信息,还有和公司关联的company_id和company_name字段,同样也违反了第三范式,因为我们只要存了company_id,就可以查询企业表,从而得到company_name。
以上说的三范式,出现的年代比较久远了,那个时候服务器的存储的成本还比较高,也就是硬盘还比较贵,所以为了节省硬盘,就应该尽量减少硬盘的使用空间。而现在硬盘已经不是昂贵的东西了,所以就出现了反三范式:
反三范式:
反三范式是基于第三范式所调整的,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。
就拿上面的第三范式的例子来说,如果我们遵守了第三范式,没有存储总计的值,那么如果我们要做统计的时候,每次都要去计算单价乘以个数来得到总计,如果表中有十万数据,就需要计算十万次,这样势必会降低效率。而反三范式就是通过冗余字段,来提高效率,只需要通过查询就可以得到结果,无需再次去逻辑运算,这也就是达到了以空间换时间的目的。
它主要包括两部分内容:基本的数据结构;对约束建模(外键、组建)。
PowerDesigner:他的优势在于:不用使用create table等语句创建表结构,数据库设计人员只关注如何进行数据建模即可,将来的数据库语句,可以自动生成。
1. 选择新建数据库模型 打开PowerDesigner,文件->建立新模型->model types(选择类型)->Physical DataModel(物理模型)
2. 控制面板
3. 创建数据库表
点即面板按钮中的创建数据库按钮创建数据库模型
切换columns标签,可以对表中的所有字段进行配置
如果基于传统的数据库设计中存在外键则可以使用面版中的Reference配置多个表之间的关联关系,效果如下图
4、导出sql语句
我们之前做的这些操作,都可以进行sql的导出,然后在数据库中执行即可:
菜单栏:Databse——》Genarate Database:
生成的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;
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证 权限管理的企业应用。
(1)创建maven工程,引入依赖
io.jsonwebtoken
jjwt
0.6.0
/**
* 通过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);
}
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);
}
}
在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;
}
}
jwt:
config:
key: saas-ihrm
ttl: 360000
(1)配置JwtUtil。修改ihrm_system工程的启动类
@Bean
public JwtUtil jwtUtil(){
return new util.JwtUtil();
}
/**
* 用户登录
* 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);
}
}
@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);
}
}
/**
* 用户登录成功之后,获取用户信息
* 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);
}
此项目采用目前比较流行的前后端分离的方式进行开发。前端是在传智播客研究院开源的前端框架(黑马Admin商用后台模板)的基础上进行的开发。
技术栈
vue 2.5++
elementUI 2.2.2
vuex
axios
vue-router
vue-i18n
前端环境
node 8.++
npm 5.++
官网上提供了非常基础的脚手架,如果我们使用官网的脚手架需要自己写很多代码比如登陆界面、主界面菜单样式等内容。 课程已经提供了功能完整的脚手架,我们可以拿过来在此基础上开发,这样可以极大节省我们开发的时间。
(1)解压提供的资源包
(2)在命令提示符进入该目录,输入命令:cnpm install
整个前端工程的工程目录结构如下:
路由和菜单是组织起一个后台应用的关键骨架。本项目侧边栏和路由是绑定在一起的,所以你只有在@/router/index.js 下面配置对应的路由,侧边栏就能动态的生成了。大大减轻了手动编辑侧边栏的工作量。当然这样就需要在配置路由的时候遵循很多的约定,这里的路由分为两种, constantRouterMap 和 asyncRouterMap 。
constantRouterMap 代通用页面。
asyncRouterMap 代表那些业务中通过 addRouters 动态添加的页面。
一个完整的前端 UI 交互到服务端处理流程是这样的:
1. UI 组件交互操作;
2. 调用统一管理的 api service 请求函数;
3. 使用封装的 request.js 发送请求;
4. 获取服务端返回;
5. 更新 data;
从上面的流程可以看出,为了方便管理维护,统一的请求处理都放在 src/api 文件夹中,并且一般按照 model纬度进行拆分文件
api/
frame.js
menus.js
users.js
permissions.js
...
在通用页面配置企业管理模块,完成企业的基本操作
(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:
(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) //访问企业列表
编辑 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) ///注册 刚新添加的 企业管理
打开刚才自动创建的 /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}
}
]
}
]
创建 /src/module-saas-clients/pages/index.vue
saas企业管理
注意文件名 驼峰格式 首字小写
页面请放在目录 /src/module-saas-clients/pages/
组件请放在目录 /src/module-saas-clients/components/
页面路由请修改 /src/module-saas-clients/router/index.js
在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)
想要显示序号只需要吧prop改为type="index"
查看
(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开关
审核
拒绝
账户信息
交易记录
(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/"'
})
理解RBAC模型的基本概念及设计思路
了解SAAS-HRM中权限控制的需求及表结构分析
完成组织机构的基本CRUD操作
实现企业组织结构管理,实现部门的基本CRUD操作
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) 在公共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)实体类
在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)使用命令行创建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}
}
]
}
在 /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)构造基本页面样式
找到 /module-departments/page/index.vue
,使用element-ui
提供的Card组件构造卡片式容器